1use serde::{Deserialize, Serialize};
448use std::collections::{HashMap, HashSet};
449use uuid::Uuid;
450
451use crate::graph::{
452 StaticCard, StaticCardsXNodesXWidgets, StaticEdge, StaticGraph, StaticNode, StaticNodegroup,
453 StaticTranslatableString,
454};
455
456const ALIZARIN_NAMESPACE: &str = "1a79f1c8-9505-4bea-a18e-28a053f725ca";
463
464pub fn generate_uuid_v5(group: (&str, Option<&str>), key: &str) -> String {
472 generate_uuid_v5_with_ns(ALIZARIN_NAMESPACE, group, key)
473}
474
475pub fn generate_uuid_v5_with_ns(
481 base_namespace_str: &str,
482 group: (&str, Option<&str>),
483 key: &str,
484) -> String {
485 let namespace_str = match group.1 {
487 Some(id) => format!("{}/{}", group.0, id),
488 None => group.0.to_string(),
489 };
490
491 let base_namespace = Uuid::parse_str(base_namespace_str).expect("Invalid base namespace");
493 let namespace = Uuid::new_v5(&base_namespace, namespace_str.as_bytes());
494
495 Uuid::new_v5(&namespace, key.as_bytes()).to_string()
497}
498
499pub fn slugify(name: &str) -> String {
508 name.to_lowercase().replace(' ', "_")
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct Widget {
518 pub id: String,
519 pub name: String,
520 pub datatype: String,
521 pub default_config: serde_json::Value,
522}
523
524impl Widget {
525 pub fn new(id: &str, name: &str, datatype: &str, default_config_json: &str) -> Self {
526 Self {
527 id: id.to_string(),
528 name: name.to_string(),
529 datatype: datatype.to_string(),
530 default_config: serde_json::from_str(default_config_json)
531 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
532 }
533 }
534
535 pub fn get_default_config(&self) -> serde_json::Value {
537 self.default_config.clone()
538 }
539}
540
541impl From<crate::registry::RegisteredWidget> for Widget {
543 fn from(registered: crate::registry::RegisteredWidget) -> Self {
544 Self {
545 id: registered.id,
546 name: registered.name,
547 datatype: registered.datatype,
548 default_config: registered.default_config,
549 }
550 }
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct CardComponent {
556 pub id: String,
557 pub name: String,
558}
559
560impl CardComponent {
561 pub fn new(id: &str, name: &str) -> Self {
562 Self {
563 id: id.to_string(),
564 name: name.to_string(),
565 }
566 }
567}
568
569pub const DEFAULT_CARD_COMPONENT_ID: &str = "f05e4d3a-53c1-11e8-b0ea-784f435179ea";
571pub const DEFAULT_CARD_COMPONENT_NAME: &str = "Default Card";
572
573pub fn default_card_component() -> CardComponent {
575 CardComponent::new(DEFAULT_CARD_COMPONENT_ID, DEFAULT_CARD_COMPONENT_NAME)
576}
577
578lazy_static::lazy_static! {
583 pub static ref WIDGETS: HashMap<String, Widget> = {
585 let mut m = HashMap::new();
586 m.insert("text-widget".to_string(), Widget::new(
587 "10000000-0000-0000-0000-000000000001",
588 "text-widget",
589 "string",
590 r#"{ "placeholder": "Enter text", "width": "100%", "maxLength": null}"#
591 ));
592 m.insert("concept-select-widget".to_string(), Widget::new(
593 "10000000-0000-0000-0000-000000000002",
594 "concept-select-widget",
595 "concept",
596 r#"{ "placeholder": "Select an option", "options": [] }"#
597 ));
598 m.insert("resource-instance-multiselect-widget".to_string(), Widget::new(
599 "ff3c400a-76ec-11e7-a793-784f435179ea",
600 "resource-instance-multiselect-widget",
601 "resource-instance-list",
602 r#"{ "placeholder": "Select an option", "options": [] }"#
603 ));
604 m.insert("concept-multiselect-widget".to_string(), Widget::new(
605 "10000000-0000-0000-0000-000000000012",
606 "concept-multiselect-widget",
607 "concept-list",
608 r#"{ "placeholder": "Select an option", "options": [] }"#
609 ));
610 m.insert("domain-select-widget".to_string(), Widget::new(
611 "10000000-0000-0000-0000-000000000015",
612 "domain-select-widget",
613 "domain-value",
614 r#"{ "placeholder": "Select an option" }"#
615 ));
616 m.insert("domain-multiselect-widget".to_string(), Widget::new(
617 "10000000-0000-0000-0000-000000000016",
618 "domain-multiselect-widget",
619 "domain-value-list",
620 r#"{ "placeholder": "Select an option" }"#
621 ));
622 m.insert("switch-widget".to_string(), Widget::new(
623 "10000000-0000-0000-0000-000000000003",
624 "switch-widget",
625 "boolean",
626 r#"{ "subtitle": "Click to switch"}"#
627 ));
628 m.insert("datepicker-widget".to_string(), Widget::new(
629 "10000000-0000-0000-0000-000000000004",
630 "datepicker-widget",
631 "date",
632 r#"{
633 "placeholder": "Enter date",
634 "viewMode": "days",
635 "dateFormat": "YYYY-MM-DD",
636 "minDate": false,
637 "maxDate": false
638 }"#
639 ));
640 m.insert("rich-text-widget".to_string(), Widget::new(
641 "10000000-0000-0000-0000-000000000005",
642 "rich-text-widget",
643 "string",
644 r#"{}"#
645 ));
646 m.insert("radio-boolean-widget".to_string(), Widget::new(
647 "10000000-0000-0000-0000-000000000006",
648 "radio-boolean-widget",
649 "boolean",
650 r#"{"trueLabel": "Yes", "falseLabel": "No"}"#
651 ));
652 m.insert("map-widget".to_string(), Widget::new(
653 "10000000-0000-0000-0000-000000000007",
654 "map-widget",
655 "geojson-feature-collection",
656 r#"{
657 "basemap": "streets",
658 "geometryTypes": [{"text":"Point", "id":"Point"}, {"text":"Line", "id":"Line"}, {"text":"Polygon", "id":"Polygon"}],
659 "overlayConfigs": [],
660 "overlayOpacity": 0.0,
661 "geocodeProvider": "MapzenGeocoder",
662 "zoom": 0,
663 "maxZoom": 20,
664 "minZoom": 0,
665 "centerX": 0,
666 "centerY": 0,
667 "pitch": 0.0,
668 "bearing": 0.0,
669 "geocodePlaceholder": "Search",
670 "geocoderVisible": true,
671 "featureColor": null,
672 "featureLineWidth": null,
673 "featurePointSize": null
674 }"#
675 ));
676 m.insert("number-widget".to_string(), Widget::new(
677 "10000000-0000-0000-0000-000000000008",
678 "number-widget",
679 "number",
680 r#"{ "placeholder": "Enter number", "width": "100%", "min":"", "max":""}"#
681 ));
682 m.insert("concept-radio-widget".to_string(), Widget::new(
683 "10000000-0000-0000-0000-000000000009",
684 "concept-radio-widget",
685 "concept",
686 r#"{ "options": [] }"#
687 ));
688 m.insert("concept-checkbox-widget".to_string(), Widget::new(
689 "10000000-0000-0000-0000-000000000013",
690 "concept-checkbox-widget",
691 "concept-list",
692 r#"{ "options": [] }"#
693 ));
694 m.insert("domain-radio-widget".to_string(), Widget::new(
695 "10000000-0000-0000-0000-000000000017",
696 "domain-radio-widget",
697 "domain-value",
698 r#"{}"#
699 ));
700 m.insert("domain-checkbox-widget".to_string(), Widget::new(
701 "10000000-0000-0000-0000-000000000018",
702 "domain-checkbox-widget",
703 "domain-value-list",
704 r#"{}"#
705 ));
706 m.insert("file-widget".to_string(), Widget::new(
707 "10000000-0000-0000-0000-000000000019",
708 "file-widget",
709 "file-list",
710 r#"{"acceptedFiles": "", "maxFilesize": "200"}"#
711 ));
712 m.insert("urldatatype-widget".to_string(), Widget::new(
713 "ca0c43ff-af73-4349-bafd-53ff9f22eebd",
714 "urldatatype-widget",
715 "url",
716 r#"{ "placeholder": "Enter URL", "url_placeholder": "Enter URL", "url_label_placeholder": "Enter URL label" }"#
717 ));
718 m.insert("resource-instance-select-widget".to_string(), Widget::new(
719 "31f3728c-7613-11e7-a139-784f435179ea",
720 "resource-instance-select-widget",
721 "resource-instance",
722 r#"{ "placeholder": "Select a resource" }"#
723 ));
724 m.insert("edtf-widget".to_string(), Widget::new(
725 "10000000-0000-0000-0000-000000000010",
726 "edtf-widget",
727 "edtf",
728 r#"{ "placeholder": "Enter EDTF date" }"#
729 ));
730 m.insert("non-localized-text-widget".to_string(), Widget::new(
731 "10000000-0000-0000-0000-000000000011",
732 "non-localized-text-widget",
733 "non-localized-string",
734 r#"{ "placeholder": "Enter text", "width": "100%" }"#
735 ));
736 m
737 };
738}
739
740lazy_static::lazy_static! {
741 pub static ref WIDGET_BY_ID: HashMap<String, String> = {
743 WIDGETS.iter().map(|(name, w)| (w.id.clone(), name.clone())).collect()
744 };
745}
746
747pub fn get_widget_name_by_id(widget_id: &str) -> Option<String> {
751 if let Some(name) = WIDGET_BY_ID.get(widget_id) {
753 return Some(name.clone());
754 }
755 for name in crate::registry::registered_widgets() {
757 if let Some(widget) = crate::registry::get_registered_widget(&name) {
758 if widget.id == widget_id {
759 return Some(name);
760 }
761 }
762 }
763 None
764}
765
766pub fn get_default_widget_for_datatype(datatype: &str) -> Result<Widget, MutationError> {
773 if let Some(widget_name) = crate::registry::get_widget_for_datatype(datatype) {
775 if let Some(registered) = crate::registry::get_registered_widget(&widget_name) {
777 return Ok(Widget::from(registered));
778 }
779 return WIDGETS
781 .get(&widget_name)
782 .cloned()
783 .ok_or(MutationError::WidgetNotFound(widget_name));
784 }
785
786 let widget_name = match datatype {
788 "number" => "number-widget",
789 "string" => "text-widget",
790 "concept" => "concept-select-widget",
791 "concept-list" => "concept-multiselect-widget",
792 "resource-instance-list" => "resource-instance-multiselect-widget",
793 "domain-value" => "domain-select-widget",
794 "domain-value-list" => "domain-multiselect-widget",
795 "geojson-feature-collection" => "map-widget",
796 "boolean" => "switch-widget",
797 "date" => "datepicker-widget",
798 "url" => "urldatatype-widget",
799 "resource-instance" => "resource-instance-select-widget",
800 "edtf" => "edtf-widget",
801 "non-localized-string" => "non-localized-text-widget",
802 "file-list" => "file-widget",
803 "semantic" => return Err(MutationError::NoWidgetForDatatype(datatype.to_string())),
804 other => return Err(MutationError::NoWidgetForDatatype(other.to_string())),
805 };
806
807 WIDGETS
808 .get(widget_name)
809 .cloned()
810 .ok_or_else(|| MutationError::WidgetNotFound(widget_name.to_string()))
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
819#[serde(from = "String")]
820pub enum Cardinality {
821 One,
823 N,
825}
826
827impl Cardinality {
828 pub fn as_str(&self) -> &'static str {
829 match self {
830 Cardinality::One => "1",
831 Cardinality::N => "n",
832 }
833 }
834}
835
836impl From<&str> for Cardinality {
837 fn from(s: &str) -> Self {
838 match s.to_lowercase().as_str() {
839 "1" | "one" => Cardinality::One,
840 _ => Cardinality::N,
841 }
842 }
843}
844
845impl From<String> for Cardinality {
846 fn from(s: String) -> Self {
847 Cardinality::from(s.as_str())
848 }
849}
850
851#[derive(Debug, Clone)]
857pub enum MutationError {
858 ParentNotFound(String),
860 NodeNotFound(String),
862 NodegroupNotFound(String),
864 CardNotFound(String),
866 CardAlreadyExists(String),
868 NoWidgetForDatatype(String),
870 WidgetNotFound(String),
872 JsonError(String),
874 AliasClash(String),
876 BranchHasNoRoot,
878 InvalidSubgraph(String),
880 InconsistentBranchPublication {
882 expected: String,
883 found: Option<String>,
884 node_id: String,
885 },
886 NoBranchNodesFound(String),
888 InvalidDatatype {
890 expected: String,
891 found: String,
892 node_id: String,
893 },
894 FunctionNotFound(String),
896 CannotDeleteRootNode(String),
898 NodeHasDependentWidgets(String),
900 AliasAlreadyExists(String),
902 InvalidConfig { alias: String, error: String },
904 ExtensionNotFound(String),
906 NoExtensionRegistry(String),
908 OntologyValidation(crate::ontology::OntologyValidationDetail),
910 Other(String),
912}
913
914impl std::fmt::Display for MutationError {
915 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
916 match self {
917 MutationError::ParentNotFound(alias) => write!(f, "Parent node not found: {}", alias),
918 MutationError::NodeNotFound(id) => write!(f, "Node not found: {}", id),
919 MutationError::NodegroupNotFound(id) => write!(f, "Nodegroup not found: {}", id),
920 MutationError::CardNotFound(ng) => write!(f, "Card not found for nodegroup: {}", ng),
921 MutationError::CardAlreadyExists(ng) => {
922 write!(f, "Nodegroup already has a card: {}", ng)
923 }
924 MutationError::NoWidgetForDatatype(dt) => {
925 write!(f, "No default widget for datatype: {} (is the relevant extension loaded? e.g. import alizarin_clm)", dt)
926 }
927 MutationError::WidgetNotFound(name) => write!(f, "Widget not found: {}", name),
928 MutationError::JsonError(msg) => write!(f, "JSON error: {}", msg),
929 MutationError::AliasClash(alias) => {
930 write!(f, "Alias already exists in target graph: {}", alias)
931 }
932 MutationError::BranchHasNoRoot => write!(f, "Branch has no root node"),
933 MutationError::InvalidSubgraph(msg) => write!(f, "Invalid subgraph: {}", msg),
934 MutationError::InconsistentBranchPublication {
935 expected,
936 found,
937 node_id,
938 } => {
939 write!(
940 f,
941 "Inconsistent branch publication ID at node {}: expected {}, found {:?}",
942 node_id, expected, found
943 )
944 }
945 MutationError::NoBranchNodesFound(target_id) => {
946 write!(f, "No branch nodes found at target: {}", target_id)
947 }
948 MutationError::InvalidDatatype {
949 expected,
950 found,
951 node_id,
952 } => {
953 write!(
954 f,
955 "Invalid datatype for node {}: expected {}, found {}",
956 node_id, expected, found
957 )
958 }
959 MutationError::FunctionNotFound(id) => write!(f, "Function mapping not found: {}", id),
960 MutationError::CannotDeleteRootNode(id) => write!(f, "Cannot delete root node: {}", id),
961 MutationError::NodeHasDependentWidgets(id) => {
962 write!(f, "Node has dependent widgets, cannot change type: {}", id)
963 }
964 MutationError::AliasAlreadyExists(alias) => {
965 write!(f, "Alias already exists: {}", alias)
966 }
967 MutationError::InvalidConfig { alias, error } => {
968 write!(f, "Invalid config for node '{}': {}", alias, error)
969 }
970 MutationError::ExtensionNotFound(name) => {
971 write!(f, "Extension mutation not found: {}", name)
972 }
973 MutationError::NoExtensionRegistry(name) => write!(
974 f,
975 "Extension mutation '{}' used but no registry provided",
976 name
977 ),
978 MutationError::OntologyValidation(detail) => {
979 write!(f, "Ontology validation error: {}", detail)
980 }
981 MutationError::Other(msg) => write!(f, "{}", msg),
982 }
983 }
984}
985
986impl std::error::Error for MutationError {}
987
988#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct AddNodeParams {
995 pub parent_alias: Option<String>,
996 pub alias: String,
997 pub name: String,
998 pub cardinality: Cardinality,
999 pub datatype: String,
1000 #[serde(default, with = "crate::graph::serde_helpers::optional_string_or_vec")]
1003 pub ontology_class: Option<Vec<String>>,
1004 pub parent_property: String,
1005 pub description: Option<String>,
1006 pub config: Option<serde_json::Value>,
1007 pub options: NodeOptions,
1008}
1009
1010#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1014pub struct NodeOptions {
1015 pub exportable: Option<bool>,
1017 pub fieldname: Option<String>,
1019 pub hascustomalias: Option<bool>,
1021 pub is_collector: Option<bool>,
1027 pub isrequired: Option<bool>,
1029 pub issearchable: Option<bool>,
1031 pub istopnode: Option<bool>,
1033 pub sortorder: Option<i32>,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039pub struct AddCardParams {
1040 pub nodegroup_id: String,
1041 pub name: StaticTranslatableString,
1042 pub component_id: Option<String>,
1043 pub options: CardOptions,
1044 pub config: Option<serde_json::Value>,
1045}
1046
1047#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1049pub struct CardOptions {
1050 pub active: Option<bool>,
1051 pub cssclass: Option<String>,
1052 pub helpenabled: Option<bool>,
1053 pub helptext: Option<StaticTranslatableString>,
1054 pub helptitle: Option<StaticTranslatableString>,
1055 pub instructions: Option<StaticTranslatableString>,
1056 pub is_editable: Option<bool>,
1057 pub description: Option<StaticTranslatableString>,
1058 pub sortorder: Option<i32>,
1059 pub visible: Option<bool>,
1060}
1061
1062#[derive(Debug, Clone, Serialize, Deserialize)]
1064pub struct AddWidgetParams {
1065 pub node_id: String,
1066 pub widget_id: String,
1067 pub label: String,
1068 pub config: serde_json::Value,
1069 pub sortorder: Option<i32>,
1070 pub visible: Option<bool>,
1071}
1072
1073#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub struct AddNodegroupParams {
1076 pub parent_alias: Option<String>,
1077 pub nodegroup_id: String,
1078 pub cardinality: Cardinality,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083pub struct AddEdgeParams {
1084 pub from_node_id: String,
1085 pub to_node_id: String,
1086 pub ontology_property: String,
1087 pub name: Option<String>,
1088 pub description: Option<String>,
1089}
1090
1091#[derive(Debug, Clone, Serialize, Deserialize)]
1093pub struct AddSubgraphParams {
1094 pub subgraph: StaticGraph,
1096 pub target_node_id: String,
1098 pub ontology_property: String,
1100 #[serde(default)]
1102 pub alias_suffix: Option<String>,
1103 #[serde(default)]
1105 pub alias_prefix: Option<String>,
1106 #[serde(default)]
1108 pub name_prefix: Option<String>,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1119pub struct UpdateSubgraphParams {
1120 pub subgraph: StaticGraph,
1122 pub target_node_id: String,
1124 pub ontology_property: String,
1126 #[serde(default)]
1128 pub alias_suffix: Option<String>,
1129 #[serde(default)]
1132 pub remove_orphaned: bool,
1133 #[serde(default)]
1137 pub alias_prefix: Option<String>,
1138 #[serde(default)]
1141 pub name_prefix: Option<String>,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1146pub struct ConceptChangeCollectionParams {
1147 pub node_id: String,
1149 pub collection_id: String,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1155pub struct DeleteCardParams {
1156 pub card_id: String,
1158}
1159
1160#[derive(Debug, Clone, Serialize, Deserialize)]
1162pub struct DeleteWidgetParams {
1163 pub widget_mapping_id: String,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1169pub struct SetDescriptorFunctionParams {
1170 pub function_id: String,
1173}
1174
1175#[derive(Debug, Clone, Serialize, Deserialize)]
1177pub struct AddFunctionParams {
1178 pub function_id: String,
1181 #[serde(default)]
1183 pub config: Option<serde_json::Value>,
1184}
1185
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1188pub struct DeleteFunctionParams {
1189 pub function_mapping_id: String,
1191}
1192
1193#[derive(Debug, Clone, Serialize, Deserialize)]
1195pub struct SetDescriptorTemplateParams {
1196 pub descriptor_type: String,
1198 pub string_template: String,
1200}
1201
1202#[derive(Debug, Clone, Serialize, Deserialize)]
1204pub struct DeleteNodeParams {
1205 pub node_id: String,
1207}
1208
1209#[derive(Debug, Clone, Serialize, Deserialize)]
1211pub struct DeleteNodegroupParams {
1212 pub nodegroup_id: String,
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize)]
1218pub struct UpdateNodeParams {
1219 pub node_id: String,
1221 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub name: Option<String>,
1224 #[serde(
1228 default,
1229 skip_serializing_if = "Option::is_none",
1230 with = "crate::graph::serde_helpers::optional_string_or_vec"
1231 )]
1232 pub ontology_class: Option<Vec<String>>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub parent_property: Option<String>,
1236 #[serde(default, skip_serializing_if = "Option::is_none")]
1238 pub description: Option<String>,
1239 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub config: Option<serde_json::Value>,
1242 #[serde(default)]
1244 pub options: UpdateNodeOptions,
1245}
1246
1247#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1249pub struct UpdateNodeOptions {
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub exportable: Option<bool>,
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub fieldname: Option<String>,
1254 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub isrequired: Option<bool>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub issearchable: Option<bool>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub sortorder: Option<i32>,
1260}
1261
1262#[derive(Debug, Clone, Serialize, Deserialize)]
1264pub struct ChangeNodeTypeParams {
1265 pub node_id: String,
1267 pub datatype: String,
1269 #[serde(default, skip_serializing_if = "Option::is_none")]
1271 pub name: Option<String>,
1272 #[serde(
1276 default,
1277 skip_serializing_if = "Option::is_none",
1278 with = "crate::graph::serde_helpers::optional_string_or_vec"
1279 )]
1280 pub ontology_class: Option<Vec<String>>,
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub parent_property: Option<String>,
1284 #[serde(default, skip_serializing_if = "Option::is_none")]
1286 pub description: Option<String>,
1287 #[serde(default, skip_serializing_if = "Option::is_none")]
1289 pub config: Option<serde_json::Value>,
1290 #[serde(default)]
1292 pub options: UpdateNodeOptions,
1293}
1294
1295#[derive(Debug, Clone, Serialize, Deserialize)]
1297pub struct RenameNodeParams {
1298 pub node_id: String,
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub alias: Option<String>,
1303 #[serde(default, skip_serializing_if = "Option::is_none")]
1305 pub name: Option<String>,
1306 #[serde(default, skip_serializing_if = "Option::is_none")]
1308 pub description: Option<String>,
1309 #[serde(default = "default_true")]
1311 pub realign_card: bool,
1312}
1313
1314#[derive(Debug, Clone, Serialize, Deserialize)]
1316pub struct RenameCardParams {
1317 pub card_id: String,
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1321 pub language: Option<String>,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub name: Option<String>,
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1329 pub name_i18n: Option<HashMap<String, String>>,
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub description: Option<String>,
1333 #[serde(default, skip_serializing_if = "Option::is_none")]
1335 pub description_i18n: Option<HashMap<String, String>>,
1336}
1337
1338#[derive(Debug, Clone, Serialize, Deserialize)]
1340pub struct RealignCardFromNodeParams {
1341 pub node_alias: String,
1343}
1344
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1347pub struct ChangeCardinalityParams {
1348 pub node_id: String,
1350 pub cardinality: Cardinality,
1352}
1353
1354#[derive(Debug, Clone, Serialize, Deserialize)]
1356pub struct CreateGraphParams {
1357 pub name: String,
1359 pub is_resource: bool,
1361 pub root_alias: String,
1363 #[serde(default, with = "crate::graph::serde_helpers::optional_string_or_vec")]
1366 pub root_ontology_class: Option<Vec<String>>,
1367 #[serde(default, skip_serializing_if = "Option::is_none")]
1369 pub graph_id: Option<String>,
1370 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub author: Option<String>,
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1375 pub description: Option<String>,
1376 #[serde(default, with = "crate::graph::serde_helpers::optional_string_or_vec")]
1379 pub ontology_id: Option<Vec<String>>,
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize)]
1384pub struct RenameGraphParams {
1385 #[serde(default, skip_serializing_if = "Option::is_none")]
1388 pub name: Option<std::collections::HashMap<String, String>>,
1389 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub description: Option<std::collections::HashMap<String, String>>,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub subtitle: Option<std::collections::HashMap<String, String>>,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1399 pub author: Option<String>,
1400}
1401
1402#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct CoppiceSubgraphParams {
1421 pub subject: String,
1423 pub publication_id: String,
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize)]
1445pub struct UpdateWidgetConfigParams {
1446 pub node_id: String,
1448 pub config: serde_json::Value,
1450}
1451
1452#[derive(Debug, Clone, Serialize, Deserialize)]
1475pub struct ExtensionMutationParams {
1476 pub name: String,
1480
1481 pub params: serde_json::Value,
1485
1486 #[serde(default = "default_extension_conformance")]
1490 pub conformance: MutationConformance,
1491}
1492
1493fn default_extension_conformance() -> MutationConformance {
1494 MutationConformance::AlwaysConformant
1495}
1496
1497pub trait ExtensionMutationHandler: Send + Sync {
1527 fn apply(
1529 &self,
1530 graph: &mut StaticGraph,
1531 params: &serde_json::Value,
1532 options: &MutatorOptions,
1533 ) -> Result<(), MutationError>;
1534
1535 fn conformance(&self) -> MutationConformance;
1539
1540 fn description(&self) -> &str {
1542 "Extension mutation"
1543 }
1544}
1545
1546pub struct ExtensionMutationRegistry {
1571 handlers: std::collections::HashMap<String, std::sync::Arc<dyn ExtensionMutationHandler>>,
1572}
1573
1574impl ExtensionMutationRegistry {
1575 pub fn new() -> Self {
1577 Self {
1578 handlers: std::collections::HashMap::new(),
1579 }
1580 }
1581
1582 pub fn register(
1588 &mut self,
1589 name: impl Into<String>,
1590 handler: std::sync::Arc<dyn ExtensionMutationHandler>,
1591 ) {
1592 self.handlers.insert(name.into(), handler);
1593 }
1594
1595 pub fn get(&self, name: &str) -> Option<&std::sync::Arc<dyn ExtensionMutationHandler>> {
1597 self.handlers.get(name)
1598 }
1599
1600 pub fn has(&self, name: &str) -> bool {
1602 self.handlers.contains_key(name)
1603 }
1604
1605 pub fn list(&self) -> Vec<&str> {
1607 self.handlers.keys().map(|s| s.as_str()).collect()
1608 }
1609}
1610
1611impl Default for ExtensionMutationRegistry {
1612 fn default() -> Self {
1613 Self::new()
1614 }
1615}
1616
1617impl std::fmt::Debug for ExtensionMutationRegistry {
1618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1619 f.debug_struct("ExtensionMutationRegistry")
1620 .field("handlers", &self.handlers.keys().collect::<Vec<_>>())
1621 .finish()
1622 }
1623}
1624
1625#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1633pub enum MutationConformance {
1634 AlwaysConformant,
1636 BranchConformant,
1638 ModelConformant,
1640 NonConformant,
1642}
1643
1644#[derive(Debug, Clone, Serialize, Deserialize)]
1646pub enum GraphMutation {
1647 AddNode(AddNodeParams),
1648 AddNodegroup(AddNodegroupParams),
1649 AddEdge(AddEdgeParams),
1650 AddCard(AddCardParams),
1651 AddWidgetToCard(AddWidgetParams),
1652 AddSubgraph(AddSubgraphParams),
1653 UpdateSubgraph(UpdateSubgraphParams),
1654 ConceptChangeCollection(ConceptChangeCollectionParams),
1655 DeleteCard(DeleteCardParams),
1656 DeleteWidget(DeleteWidgetParams),
1657 AddFunction(AddFunctionParams),
1658 SetDescriptorFunction(SetDescriptorFunctionParams),
1660 DeleteFunction(DeleteFunctionParams),
1661 DeleteNode(DeleteNodeParams),
1662 DeleteNodegroup(DeleteNodegroupParams),
1663 UpdateNode(UpdateNodeParams),
1664 ChangeNodeType(ChangeNodeTypeParams),
1665 ChangeCardinality(ChangeCardinalityParams),
1666 RenameNode(RenameNodeParams),
1667 RenameCard(RenameCardParams),
1669 RealignCardFromNode(RealignCardFromNodeParams),
1671 RenameGraph(RenameGraphParams),
1673 SetDescriptorTemplate(SetDescriptorTemplateParams),
1675 CreateGraph(CreateGraphParams),
1677 CoppiceSubgraph(CoppiceSubgraphParams),
1679 UpdateWidgetConfig(UpdateWidgetConfigParams),
1681 Extension(ExtensionMutationParams),
1683}
1684
1685impl GraphMutation {
1686 pub fn conformance(&self) -> MutationConformance {
1688 match self {
1689 GraphMutation::AddNode(_) => MutationConformance::BranchConformant,
1691 GraphMutation::AddNodegroup(_) => MutationConformance::BranchConformant,
1692 GraphMutation::AddEdge(_) => MutationConformance::BranchConformant,
1693 GraphMutation::AddCard(_) => MutationConformance::BranchConformant,
1694 GraphMutation::AddWidgetToCard(_) => MutationConformance::BranchConformant,
1695 GraphMutation::AddSubgraph(_) => MutationConformance::ModelConformant,
1697 GraphMutation::UpdateSubgraph(_) => MutationConformance::ModelConformant,
1698 GraphMutation::ConceptChangeCollection(_) => MutationConformance::AlwaysConformant,
1700 GraphMutation::DeleteCard(_) => MutationConformance::AlwaysConformant,
1702 GraphMutation::DeleteWidget(_) => MutationConformance::AlwaysConformant,
1703 GraphMutation::AddFunction(_) => MutationConformance::ModelConformant,
1704 GraphMutation::SetDescriptorFunction(_) => MutationConformance::ModelConformant,
1705 GraphMutation::DeleteFunction(_) => MutationConformance::AlwaysConformant,
1706 GraphMutation::DeleteNode(_) => MutationConformance::AlwaysConformant,
1707 GraphMutation::DeleteNodegroup(_) => MutationConformance::AlwaysConformant,
1708 GraphMutation::UpdateNode(_) => MutationConformance::BranchConformant,
1710 GraphMutation::ChangeNodeType(_) => MutationConformance::BranchConformant,
1711 GraphMutation::ChangeCardinality(_) => MutationConformance::BranchConformant,
1712 GraphMutation::RenameNode(_) => MutationConformance::AlwaysConformant,
1713 GraphMutation::RenameCard(_) => MutationConformance::AlwaysConformant,
1714 GraphMutation::RealignCardFromNode(_) => MutationConformance::AlwaysConformant,
1715 GraphMutation::RenameGraph(_) => MutationConformance::AlwaysConformant,
1717 GraphMutation::SetDescriptorTemplate(_) => MutationConformance::AlwaysConformant,
1719 GraphMutation::CreateGraph(_) => MutationConformance::NonConformant,
1721 GraphMutation::CoppiceSubgraph(_) => MutationConformance::AlwaysConformant,
1723 GraphMutation::UpdateWidgetConfig(_) => MutationConformance::AlwaysConformant,
1725 GraphMutation::Extension(params) => params.conformance,
1727 }
1728 }
1729}
1730
1731#[derive(Debug, Clone)]
1737pub struct MutatorOptions {
1738 pub autocreate_card: bool,
1740 pub autocreate_widget: bool,
1742 pub ontology_validator: Option<crate::ontology::OntologyValidator>,
1744 pub skip_publication: bool,
1748}
1749
1750impl Default for MutatorOptions {
1751 fn default() -> Self {
1752 Self {
1753 autocreate_card: true,
1754 autocreate_widget: true,
1755 ontology_validator: None,
1756 skip_publication: false,
1757 }
1758 }
1759}
1760
1761fn normalise_class_arg(class: &str) -> Option<Vec<String>> {
1766 sanitize_class_list(Some(vec![class.to_string()]))
1767}
1768
1769fn sanitize_class_list(list: Option<Vec<String>>) -> Option<Vec<String>> {
1772 match list {
1773 None => None,
1774 Some(v) => {
1775 let cleaned: Vec<String> = v.into_iter().filter(|s| !s.trim().is_empty()).collect();
1776 if cleaned.is_empty() {
1777 None
1778 } else {
1779 Some(cleaned)
1780 }
1781 }
1782 }
1783}
1784
1785pub struct GraphMutator {
1795 base_graph: StaticGraph,
1796 mutations: Vec<GraphMutation>,
1797 options: MutatorOptions,
1798}
1799
1800impl GraphMutator {
1801 pub fn new(base_graph: StaticGraph) -> Self {
1803 Self {
1804 base_graph,
1805 mutations: Vec::new(),
1806 options: MutatorOptions::default(),
1807 }
1808 }
1809
1810 pub fn with_options(base_graph: StaticGraph, options: MutatorOptions) -> Self {
1812 Self {
1813 base_graph,
1814 mutations: Vec::new(),
1815 options,
1816 }
1817 }
1818
1819 pub fn mutations(&self) -> &[GraphMutation] {
1821 &self.mutations
1822 }
1823
1824 #[allow(clippy::too_many_arguments)]
1830 pub fn add_semantic_node(
1831 mut self,
1832 parent_alias: Option<&str>,
1833 alias: &str,
1834 name: &str,
1835 cardinality: Cardinality,
1836 ontology_class: &str,
1837 parent_property: &str,
1838 description: Option<&str>,
1839 options: NodeOptions,
1840 config: Option<serde_json::Value>,
1841 ) -> Self {
1842 self.add_generic_node_mut(
1843 parent_alias,
1844 alias,
1845 name,
1846 cardinality,
1847 "semantic",
1848 ontology_class,
1849 parent_property,
1850 description,
1851 options,
1852 config,
1853 );
1854 self
1855 }
1856
1857 #[allow(clippy::too_many_arguments)]
1859 pub fn add_string_node(
1860 mut self,
1861 parent_alias: Option<&str>,
1862 alias: &str,
1863 name: &str,
1864 cardinality: Cardinality,
1865 ontology_class: &str,
1866 parent_property: &str,
1867 description: Option<&str>,
1868 options: NodeOptions,
1869 config: Option<serde_json::Value>,
1870 ) -> Self {
1871 self.add_generic_node_mut(
1872 parent_alias,
1873 alias,
1874 name,
1875 cardinality,
1876 "string",
1877 ontology_class,
1878 parent_property,
1879 description,
1880 options,
1881 config,
1882 );
1883 self
1884 }
1885
1886 #[allow(clippy::too_many_arguments)]
1888 pub fn add_concept_node(
1889 mut self,
1890 parent_alias: Option<&str>,
1891 alias: &str,
1892 name: &str,
1893 collection_id: Option<&str>,
1894 is_list: bool,
1895 cardinality: Cardinality,
1896 ontology_class: &str,
1897 parent_property: &str,
1898 description: Option<&str>,
1899 options: NodeOptions,
1900 config: Option<serde_json::Value>,
1901 ) -> Self {
1902 let mut node_config = config.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
1903 if let Some(coll_id) = collection_id {
1904 if let serde_json::Value::Object(ref mut map) = node_config {
1905 map.insert(
1906 "rdmCollection".to_string(),
1907 serde_json::Value::String(coll_id.to_string()),
1908 );
1909 }
1910 }
1911 let datatype = if is_list { "concept-list" } else { "concept" };
1912 self.add_generic_node_mut(
1913 parent_alias,
1914 alias,
1915 name,
1916 cardinality,
1917 datatype,
1918 ontology_class,
1919 parent_property,
1920 description,
1921 options,
1922 Some(node_config),
1923 );
1924 self
1925 }
1926
1927 #[allow(clippy::too_many_arguments)]
1929 pub fn add_number_node(
1930 mut self,
1931 parent_alias: Option<&str>,
1932 alias: &str,
1933 name: &str,
1934 cardinality: Cardinality,
1935 ontology_class: &str,
1936 parent_property: &str,
1937 description: Option<&str>,
1938 options: NodeOptions,
1939 config: Option<serde_json::Value>,
1940 ) -> Self {
1941 self.add_generic_node_mut(
1942 parent_alias,
1943 alias,
1944 name,
1945 cardinality,
1946 "number",
1947 ontology_class,
1948 parent_property,
1949 description,
1950 options,
1951 config,
1952 );
1953 self
1954 }
1955
1956 #[allow(clippy::too_many_arguments)]
1958 pub fn add_date_node(
1959 mut self,
1960 parent_alias: Option<&str>,
1961 alias: &str,
1962 name: &str,
1963 cardinality: Cardinality,
1964 ontology_class: &str,
1965 parent_property: &str,
1966 description: Option<&str>,
1967 options: NodeOptions,
1968 config: Option<serde_json::Value>,
1969 ) -> Self {
1970 self.add_generic_node_mut(
1971 parent_alias,
1972 alias,
1973 name,
1974 cardinality,
1975 "date",
1976 ontology_class,
1977 parent_property,
1978 description,
1979 options,
1980 config,
1981 );
1982 self
1983 }
1984
1985 #[allow(clippy::too_many_arguments)]
1987 pub fn add_boolean_node(
1988 mut self,
1989 parent_alias: Option<&str>,
1990 alias: &str,
1991 name: &str,
1992 cardinality: Cardinality,
1993 ontology_class: &str,
1994 parent_property: &str,
1995 description: Option<&str>,
1996 options: NodeOptions,
1997 config: Option<serde_json::Value>,
1998 ) -> Self {
1999 self.add_generic_node_mut(
2000 parent_alias,
2001 alias,
2002 name,
2003 cardinality,
2004 "boolean",
2005 ontology_class,
2006 parent_property,
2007 description,
2008 options,
2009 config,
2010 );
2011 self
2012 }
2013
2014 #[allow(clippy::too_many_arguments)]
2016 pub fn add_generic_node(
2017 mut self,
2018 parent_alias: Option<&str>,
2019 alias: &str,
2020 name: &str,
2021 cardinality: Cardinality,
2022 datatype: &str,
2023 ontology_class: &str,
2024 parent_property: &str,
2025 description: Option<&str>,
2026 options: NodeOptions,
2027 config: Option<serde_json::Value>,
2028 ) -> Self {
2029 self.add_generic_node_mut(
2030 parent_alias,
2031 alias,
2032 name,
2033 cardinality,
2034 datatype,
2035 ontology_class,
2036 parent_property,
2037 description,
2038 options,
2039 config,
2040 );
2041 self
2042 }
2043
2044 #[allow(clippy::too_many_arguments)]
2046 fn add_generic_node_mut(
2047 &mut self,
2048 parent_alias: Option<&str>,
2049 alias: &str,
2050 name: &str,
2051 cardinality: Cardinality,
2052 datatype: &str,
2053 ontology_class: &str,
2054 parent_property: &str,
2055 description: Option<&str>,
2056 options: NodeOptions,
2057 config: Option<serde_json::Value>,
2058 ) {
2059 let ontology_class = normalise_class_arg(ontology_class);
2060 self.mutations.push(GraphMutation::AddNode(AddNodeParams {
2061 parent_alias: parent_alias.map(String::from),
2062 alias: alias.to_string(),
2063 name: name.to_string(),
2064 cardinality,
2065 datatype: datatype.to_string(),
2066 ontology_class,
2067 parent_property: parent_property.to_string(),
2068 description: description.map(String::from),
2069 config,
2070 options,
2071 }));
2072 }
2073
2074 pub fn add_card(
2080 mut self,
2081 nodegroup_id: &str,
2082 name: &str,
2083 options: CardOptions,
2084 config: Option<serde_json::Value>,
2085 ) -> Self {
2086 self.mutations.push(GraphMutation::AddCard(AddCardParams {
2087 nodegroup_id: nodegroup_id.to_string(),
2088 name: StaticTranslatableString::from_string(name),
2089 component_id: Some(DEFAULT_CARD_COMPONENT_ID.to_string()),
2090 options,
2091 config,
2092 }));
2093 self
2094 }
2095
2096 pub fn add_widget_to_card(
2098 mut self,
2099 node_id: &str,
2100 widget: &Widget,
2101 label: &str,
2102 config: serde_json::Value,
2103 sortorder: Option<i32>,
2104 visible: Option<bool>,
2105 ) -> Self {
2106 self.mutations
2107 .push(GraphMutation::AddWidgetToCard(AddWidgetParams {
2108 node_id: node_id.to_string(),
2109 widget_id: widget.id.clone(),
2110 label: label.to_string(),
2111 config,
2112 sortorder,
2113 visible,
2114 }));
2115 self
2116 }
2117
2118 pub fn build(self) -> Result<StaticGraph, MutationError> {
2124 let mut graph = self.base_graph.deep_clone();
2125
2126 for mutation in self.mutations {
2127 apply_mutation(&mut graph, mutation, &self.options)?;
2128 }
2129
2130 graph.build_indices();
2132
2133 Ok(graph)
2134 }
2135}
2136
2137fn apply_mutation(
2146 graph: &mut StaticGraph,
2147 mutation: GraphMutation,
2148 options: &MutatorOptions,
2149) -> Result<(), MutationError> {
2150 apply_mutation_with_extensions(graph, mutation, options, None)
2151}
2152
2153fn apply_mutation_with_extensions(
2161 graph: &mut StaticGraph,
2162 mutation: GraphMutation,
2163 options: &MutatorOptions,
2164 registry: Option<&ExtensionMutationRegistry>,
2165) -> Result<(), MutationError> {
2166 match mutation {
2167 GraphMutation::AddNode(params) => apply_add_node(graph, params, options),
2168 GraphMutation::AddNodegroup(params) => apply_add_nodegroup(graph, params, options),
2169 GraphMutation::AddEdge(params) => apply_add_edge(graph, params),
2170 GraphMutation::AddCard(params) => apply_add_card(graph, params),
2171 GraphMutation::AddWidgetToCard(params) => apply_add_widget(graph, params),
2172 GraphMutation::AddSubgraph(params) => apply_add_subgraph(graph, params),
2173 GraphMutation::UpdateSubgraph(params) => apply_update_subgraph(graph, params),
2174 GraphMutation::ConceptChangeCollection(params) => apply_concept_change_collection(graph, params),
2175 GraphMutation::DeleteCard(params) => apply_delete_card(graph, params),
2176 GraphMutation::DeleteWidget(params) => apply_delete_widget(graph, params),
2177 GraphMutation::AddFunction(params) => apply_add_function(graph, params),
2178 GraphMutation::SetDescriptorFunction(params) => apply_set_descriptor_function(graph, params),
2179 GraphMutation::DeleteFunction(params) => apply_delete_function(graph, params),
2180 GraphMutation::DeleteNode(params) => apply_delete_node(graph, params),
2181 GraphMutation::DeleteNodegroup(params) => apply_delete_nodegroup(graph, params),
2182 GraphMutation::UpdateNode(params) => apply_update_node(graph, params, options),
2183 GraphMutation::ChangeNodeType(params) => apply_change_node_type(graph, params),
2184 GraphMutation::ChangeCardinality(params) => apply_change_cardinality(graph, params),
2185 GraphMutation::RenameNode(params) => apply_rename_node(graph, params),
2186 GraphMutation::RenameCard(params) => apply_rename_card(graph, params),
2187 GraphMutation::RealignCardFromNode(params) => apply_realign_card_from_node(graph, params),
2188 GraphMutation::RenameGraph(params) => apply_rename_graph(graph, params),
2189 GraphMutation::SetDescriptorTemplate(params) => apply_set_descriptor_template(graph, params),
2190 GraphMutation::CoppiceSubgraph(params) => apply_coppice_subgraph(graph, params),
2191 GraphMutation::UpdateWidgetConfig(params) => apply_update_widget_config(graph, params),
2192 GraphMutation::CreateGraph(_) => {
2193 Err(MutationError::Other(
2194 "CreateGraph cannot be used as a regular mutation. Use apply_mutations_create_from_json instead.".to_string()
2195 ))
2196 }
2197 GraphMutation::Extension(params) => {
2198 match registry {
2199 Some(reg) => {
2200 let handler = reg.get(¶ms.name)
2201 .ok_or_else(|| MutationError::ExtensionNotFound(params.name.clone()))?;
2202 handler.apply(graph, ¶ms.params, options)
2203 }
2204 None => Err(MutationError::NoExtensionRegistry(params.name)),
2205 }
2206 }
2207 }
2208}
2209
2210fn apply_add_node(
2211 graph: &mut StaticGraph,
2212 params: AddNodeParams,
2213 options: &MutatorOptions,
2214) -> Result<(), MutationError> {
2215 if graph.find_node_by_alias(¶ms.alias).is_some() {
2217 return Err(MutationError::AliasAlreadyExists(params.alias.clone()));
2218 }
2219
2220 let parent = if let Some(ref parent_alias) = params.parent_alias {
2222 graph
2223 .find_node_by_alias(parent_alias)
2224 .ok_or_else(|| MutationError::ParentNotFound(parent_alias.clone()))?
2225 } else {
2226 graph.get_root()
2227 };
2228 let parent_nodeid = parent.nodeid.clone();
2229 let parent_nodegroup_id = parent.nodegroup_id.clone();
2230 let parent_classes: Vec<String> = parent.ontologyclass.clone().unwrap_or_default();
2231
2232 let node_classes: Option<Vec<String>> = sanitize_class_list(params.ontology_class);
2234
2235 if let Some(ref validator) = options.ontology_validator {
2237 if let Some(ref classes) = node_classes {
2238 validator
2239 .validate_edge_multi(&parent_classes, ¶ms.parent_property, classes)
2240 .map_err(MutationError::OntologyValidation)?;
2241 }
2242 }
2243
2244 let node_id = generate_uuid_v5(
2246 ("graph", Some(&graph.graphid)),
2247 &format!("node-{}", params.alias),
2248 );
2249
2250 let (nodegroup_id, created_new_nodegroup) = if params.cardinality == Cardinality::N
2257 || parent.is_root()
2258 || params.options.is_collector == Some(true)
2259 {
2260 let ng_id = node_id.clone();
2262
2263 let nodegroup = StaticNodegroup {
2265 nodegroupid: ng_id.clone(),
2266 cardinality: Some(params.cardinality.as_str().to_string()),
2267 parentnodegroup_id: parent_nodegroup_id.clone(),
2268 legacygroupid: None,
2269 grouping_node_id: None,
2270 };
2271 graph.push_nodegroup(nodegroup);
2272
2273 if options.autocreate_card {
2275 let card_id = generate_uuid_v5(
2276 ("graph", Some(&graph.graphid)),
2277 &format!("card-ng-{}", ng_id),
2278 );
2279 let card = StaticCard {
2280 active: true,
2281 cardid: card_id,
2282 component_id: DEFAULT_CARD_COMPONENT_ID.to_string(),
2283 config: None,
2284 constraints: vec![],
2285 cssclass: None,
2286 description: None,
2287 graph_id: graph.graphid.clone(),
2288 helpenabled: false,
2289 helptext: StaticTranslatableString::empty(),
2290 helptitle: StaticTranslatableString::empty(),
2291 instructions: StaticTranslatableString::empty(),
2292 is_editable: Some(true),
2293 name: StaticTranslatableString::from_string(¶ms.name),
2294 nodegroup_id: ng_id.clone(),
2295 sortorder: Some(0),
2296 visible: true,
2297 source_identifier_id: None,
2298 };
2299 graph.push_card(card);
2300 }
2301
2302 (Some(ng_id), true)
2303 } else {
2304 (parent_nodegroup_id, false)
2305 };
2306
2307 let config: HashMap<String, serde_json::Value> = match params.config {
2309 Some(v) => serde_json::from_value(v).map_err(|e| MutationError::InvalidConfig {
2310 alias: params.alias.clone(),
2311 error: e.to_string(),
2312 })?,
2313 None => HashMap::new(),
2314 };
2315
2316 let node = StaticNode {
2318 nodeid: node_id.clone(),
2319 name: params.name.clone(),
2320 alias: Some(params.alias.clone()),
2321 datatype: params.datatype.clone(),
2322 nodegroup_id: nodegroup_id.clone(),
2323 graph_id: graph.graphid.clone(),
2324 is_collector: params.options.is_collector.unwrap_or(false),
2325 isrequired: params.options.isrequired.unwrap_or(false),
2326 exportable: params.options.exportable.unwrap_or(false),
2327 sortorder: Some(params.options.sortorder.unwrap_or(0)),
2328 config,
2329 parentproperty: Some(params.parent_property.clone()),
2330 ontologyclass: node_classes,
2331 description: params
2332 .description
2333 .map(|d| StaticTranslatableString::from_string(&d)),
2334 fieldname: params.options.fieldname,
2335 hascustomalias: params.options.hascustomalias.unwrap_or(false),
2336 issearchable: params.options.issearchable.unwrap_or(true),
2337 istopnode: params.options.istopnode.unwrap_or(false),
2338 sourcebranchpublication_id: None,
2339 source_identifier_id: None,
2340 is_immutable: None,
2341 };
2342 graph.push_node(node);
2343
2344 let edge_id = generate_uuid_v5(
2346 ("graph", Some(&graph.graphid)),
2347 &format!("edge-{}-{}", parent_nodeid, node_id),
2348 );
2349 let edge = StaticEdge {
2350 domainnode_id: parent_nodeid,
2351 rangenode_id: node_id.clone(),
2352 edgeid: edge_id,
2353 graph_id: graph.graphid.clone(),
2354 name: None,
2355 ontologyproperty: Some(params.parent_property),
2356 description: None,
2357 source_identifier_id: None,
2358 };
2359 graph.push_edge(edge);
2360
2361 if options.autocreate_widget && params.datatype != "semantic" {
2363 let widget = get_default_widget_for_datatype(¶ms.datatype)
2364 .map_err(|_| MutationError::NoWidgetForDatatype(params.datatype.clone()))?;
2365
2366 let ng_id = nodegroup_id.as_ref().ok_or_else(|| {
2367 MutationError::Other(format!(
2368 "Cannot create widget for node '{}': no nodegroup",
2369 params.alias
2370 ))
2371 })?;
2372
2373 if let Some(card) = graph.find_card_by_nodegroup(ng_id) {
2376 let mut widget_config = widget.get_default_config();
2377 if let serde_json::Value::Object(ref mut map) = widget_config {
2378 map.insert(
2379 "label".to_string(),
2380 serde_json::Value::String(params.name.clone()),
2381 );
2382 }
2383
2384 let cxnxw_id = generate_uuid_v5(
2385 ("graph", Some(&graph.graphid)),
2386 &format!("cxnxw-{}-{}", node_id, widget.id),
2387 );
2388
2389 let cxnxw = StaticCardsXNodesXWidgets {
2390 card_id: card.cardid.clone(),
2391 config: widget_config,
2392 id: cxnxw_id,
2393 label: StaticTranslatableString::from_string(¶ms.name),
2394 node_id: node_id.clone(),
2395 sortorder: Some(params.options.sortorder.unwrap_or(0)),
2396 visible: true,
2397 widget_id: widget.id.clone(),
2398 source_identifier_id: None,
2399 };
2400 graph.push_card_x_node_x_widget(cxnxw);
2401 } else if created_new_nodegroup {
2402 return Err(MutationError::CardNotFound(ng_id.clone()));
2405 }
2406 }
2408
2409 Ok(())
2410}
2411
2412fn apply_add_nodegroup(
2413 graph: &mut StaticGraph,
2414 params: AddNodegroupParams,
2415 options: &MutatorOptions,
2416) -> Result<(), MutationError> {
2417 let parent_nodegroup_id = if let Some(ref parent_alias) = params.parent_alias {
2419 let parent = graph
2420 .find_node_by_alias(parent_alias)
2421 .ok_or_else(|| MutationError::ParentNotFound(parent_alias.clone()))?;
2422 parent.nodegroup_id.clone()
2423 } else {
2424 graph.get_root().nodegroup_id.clone()
2425 };
2426
2427 let nodegroup = StaticNodegroup {
2428 nodegroupid: params.nodegroup_id.clone(),
2429 cardinality: Some(params.cardinality.as_str().to_string()),
2430 parentnodegroup_id: parent_nodegroup_id,
2431 legacygroupid: None,
2432 grouping_node_id: None,
2433 };
2434 graph.push_nodegroup(nodegroup);
2435
2436 if options.autocreate_card {
2438 let card_id = generate_uuid_v5(
2439 ("graph", Some(&graph.graphid)),
2440 &format!("card-ng-{}", params.nodegroup_id),
2441 );
2442 let card = StaticCard {
2443 active: true,
2444 cardid: card_id,
2445 component_id: DEFAULT_CARD_COMPONENT_ID.to_string(),
2446 config: None,
2447 constraints: vec![],
2448 cssclass: None,
2449 description: None,
2450 graph_id: graph.graphid.clone(),
2451 helpenabled: false,
2452 helptext: StaticTranslatableString::empty(),
2453 helptitle: StaticTranslatableString::empty(),
2454 instructions: StaticTranslatableString::empty(),
2455 is_editable: Some(true),
2456 name: StaticTranslatableString::from_string("(unnamed)"),
2457 nodegroup_id: params.nodegroup_id,
2458 sortorder: Some(0),
2459 visible: true,
2460 source_identifier_id: None,
2461 };
2462 graph.push_card(card);
2463 }
2464
2465 Ok(())
2466}
2467
2468fn apply_add_edge(graph: &mut StaticGraph, params: AddEdgeParams) -> Result<(), MutationError> {
2469 let edge_id = generate_uuid_v5(
2470 ("graph", Some(&graph.graphid)),
2471 &format!("edge-{}-{}", params.from_node_id, params.to_node_id),
2472 );
2473
2474 let edge = StaticEdge {
2475 domainnode_id: params.from_node_id,
2476 rangenode_id: params.to_node_id,
2477 edgeid: edge_id,
2478 graph_id: graph.graphid.clone(),
2479 name: params.name,
2480 ontologyproperty: Some(params.ontology_property),
2481 description: params.description,
2482 source_identifier_id: None,
2483 };
2484 graph.push_edge(edge);
2485
2486 Ok(())
2487}
2488
2489fn apply_add_card(graph: &mut StaticGraph, params: AddCardParams) -> Result<(), MutationError> {
2490 let nodegroup_id = graph
2494 .find_node_by_alias(¶ms.nodegroup_id)
2495 .and_then(|n| n.nodegroup_id.clone())
2496 .unwrap_or(params.nodegroup_id);
2497
2498 if graph.find_card_by_nodegroup(&nodegroup_id).is_some() {
2500 return Err(MutationError::CardAlreadyExists(nodegroup_id));
2501 }
2502
2503 let card_id = generate_uuid_v5(
2504 ("graph", Some(&graph.graphid)),
2505 &format!("card-ng-{}", nodegroup_id),
2506 );
2507
2508 let card = StaticCard {
2509 active: params.options.active.unwrap_or(true),
2510 cardid: card_id,
2511 component_id: params
2512 .component_id
2513 .unwrap_or_else(|| DEFAULT_CARD_COMPONENT_ID.to_string()),
2514 config: params.config,
2515 constraints: vec![],
2516 cssclass: params.options.cssclass,
2517 description: params.options.description,
2518 graph_id: graph.graphid.clone(),
2519 helpenabled: params.options.helpenabled.unwrap_or(false),
2520 helptext: params
2521 .options
2522 .helptext
2523 .unwrap_or_else(StaticTranslatableString::empty),
2524 helptitle: params
2525 .options
2526 .helptitle
2527 .unwrap_or_else(StaticTranslatableString::empty),
2528 instructions: params
2529 .options
2530 .instructions
2531 .unwrap_or_else(StaticTranslatableString::empty),
2532 is_editable: params.options.is_editable,
2533 name: params.name,
2534 nodegroup_id,
2535 sortorder: Some(params.options.sortorder.unwrap_or(0)),
2536 visible: params.options.visible.unwrap_or(true),
2537 source_identifier_id: None,
2538 };
2539 graph.push_card(card);
2540
2541 Ok(())
2542}
2543
2544fn apply_add_widget(graph: &mut StaticGraph, params: AddWidgetParams) -> Result<(), MutationError> {
2545 let node = graph
2547 .nodes
2548 .iter()
2549 .find(|n| n.nodeid == params.node_id)
2550 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
2551 let nodegroup_id = node
2552 .nodegroup_id
2553 .clone()
2554 .ok_or_else(|| MutationError::NodegroupNotFound(params.node_id.clone()))?;
2555
2556 let card = graph
2558 .find_card_by_nodegroup(&nodegroup_id)
2559 .ok_or_else(|| MutationError::CardNotFound(nodegroup_id.clone()))?;
2560 let card_id = card.cardid.clone();
2561
2562 let cxnxw_id = generate_uuid_v5(
2563 ("graph", Some(&graph.graphid)),
2564 &format!("cxnxw-{}-{}", params.node_id, params.widget_id),
2565 );
2566
2567 let cxnxw = StaticCardsXNodesXWidgets {
2568 card_id,
2569 config: params.config,
2570 id: cxnxw_id,
2571 label: StaticTranslatableString::from_string(¶ms.label),
2572 node_id: params.node_id,
2573 sortorder: Some(params.sortorder.unwrap_or(0)),
2574 visible: params.visible.unwrap_or(true),
2575 widget_id: params.widget_id,
2576 source_identifier_id: None,
2577 };
2578 graph.push_card_x_node_x_widget(cxnxw);
2579
2580 Ok(())
2581}
2582
2583const CONCEPT_DATATYPES: &[&str] = &["concept", "concept-list"];
2585
2586fn apply_concept_change_collection(
2587 graph: &mut StaticGraph,
2588 params: ConceptChangeCollectionParams,
2589) -> Result<(), MutationError> {
2590 let node = graph
2592 .find_node_by_alias(¶ms.node_id)
2593 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
2594 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
2595
2596 if !CONCEPT_DATATYPES.contains(&node.datatype.as_str()) {
2598 return Err(MutationError::InvalidDatatype {
2599 expected: "concept or concept-list".to_string(),
2600 found: node.datatype.clone(),
2601 node_id: params.node_id.clone(),
2602 });
2603 }
2604
2605 let node_id = node.nodeid.clone();
2606
2607 let node_mut = graph
2609 .nodes
2610 .iter_mut()
2611 .find(|n| n.nodeid == node_id)
2612 .ok_or_else(|| MutationError::NodeNotFound(node_id.clone()))?;
2613
2614 node_mut.config.insert(
2616 "rdmCollection".to_string(),
2617 serde_json::Value::String(params.collection_id),
2618 );
2619
2620 Ok(())
2621}
2622
2623fn apply_delete_card(
2628 graph: &mut StaticGraph,
2629 params: DeleteCardParams,
2630) -> Result<(), MutationError> {
2631 let card_exists = graph
2633 .cards
2634 .as_ref()
2635 .map(|cards| cards.iter().any(|c| c.cardid == params.card_id))
2636 .unwrap_or(false);
2637
2638 if !card_exists {
2639 return Err(MutationError::CardNotFound(params.card_id));
2640 }
2641
2642 if let Some(ref mut cxnxws) = graph.cards_x_nodes_x_widgets {
2644 cxnxws.retain(|c| c.card_id != params.card_id);
2645 }
2646
2647 if let Some(ref mut cards) = graph.cards {
2649 cards.retain(|c| c.cardid != params.card_id);
2650 }
2651
2652 Ok(())
2653}
2654
2655fn apply_delete_widget(
2656 graph: &mut StaticGraph,
2657 params: DeleteWidgetParams,
2658) -> Result<(), MutationError> {
2659 let widget_exists = graph
2661 .cards_x_nodes_x_widgets
2662 .as_ref()
2663 .map(|cxnxws| cxnxws.iter().any(|c| c.id == params.widget_mapping_id))
2664 .unwrap_or(false);
2665
2666 if !widget_exists {
2667 return Err(MutationError::WidgetNotFound(params.widget_mapping_id));
2668 }
2669
2670 if let Some(ref mut cxnxws) = graph.cards_x_nodes_x_widgets {
2672 cxnxws.retain(|c| c.id != params.widget_mapping_id);
2673 }
2674
2675 Ok(())
2676}
2677
2678fn apply_update_widget_config(
2679 graph: &mut StaticGraph,
2680 params: UpdateWidgetConfigParams,
2681) -> Result<(), MutationError> {
2682 let node_id = graph
2684 .find_node_by_alias(¶ms.node_id)
2685 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
2686 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?
2687 .nodeid
2688 .clone();
2689
2690 let cxnxw = graph
2692 .cards_x_nodes_x_widgets
2693 .as_mut()
2694 .and_then(|cxnxws| cxnxws.iter_mut().find(|c| c.node_id == node_id))
2695 .ok_or_else(|| {
2696 MutationError::WidgetNotFound(format!("no widget for node {}", params.node_id))
2697 })?;
2698
2699 if let serde_json::Value::Object(patch) = params.config {
2701 if let serde_json::Value::Object(ref mut existing) = cxnxw.config {
2702 for (key, value) in patch {
2703 existing.insert(key, value);
2704 }
2705 } else {
2706 cxnxw.config = serde_json::Value::Object(patch);
2708 }
2709 } else {
2710 return Err(MutationError::Other(
2711 "update_widget_config: config must be a JSON object".to_string(),
2712 ));
2713 }
2714
2715 Ok(())
2716}
2717
2718fn resolve_function_id(raw: &str) -> String {
2720 if uuid::Uuid::parse_str(raw).is_ok() {
2721 return raw.to_string();
2722 }
2723 uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, raw.as_bytes()).to_string()
2724}
2725
2726fn apply_add_function(
2727 graph: &mut StaticGraph,
2728 params: AddFunctionParams,
2729) -> Result<(), MutationError> {
2730 use crate::graph::StaticFunctionsXGraphs;
2731
2732 let function_id = resolve_function_id(¶ms.function_id);
2733 let fxg = graph.functions_x_graphs.get_or_insert_with(Vec::new);
2734
2735 if fxg
2736 .iter()
2737 .any(|f| f.function_id == function_id && f.graph_id == graph.graphid)
2738 {
2739 return Err(MutationError::Other(format!(
2740 "Function {} already mapped to graph {}",
2741 function_id, graph.graphid
2742 )));
2743 }
2744
2745 fxg.push(StaticFunctionsXGraphs {
2746 id: generate_uuid_v5(("function", Some(&graph.graphid)), &function_id),
2747 function_id,
2748 graph_id: graph.graphid.clone(),
2749 config: params
2750 .config
2751 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
2752 });
2753
2754 Ok(())
2755}
2756
2757fn apply_set_descriptor_function(
2758 graph: &mut StaticGraph,
2759 params: SetDescriptorFunctionParams,
2760) -> Result<(), MutationError> {
2761 use crate::graph::StaticFunctionsXGraphs;
2762 use crate::graph::DESCRIPTOR_FUNCTION_ID;
2763
2764 let function_id = resolve_function_id(¶ms.function_id);
2765 let fxg = graph.functions_x_graphs.get_or_insert_with(Vec::new);
2766
2767 fxg.retain(|f| {
2772 if f.function_id == DESCRIPTOR_FUNCTION_ID {
2773 return false;
2774 }
2775 if f.function_id == function_id {
2776 return false; }
2778 if f.config
2779 .as_object()
2780 .is_some_and(|c| c.contains_key("descriptor_types"))
2781 {
2782 return false;
2783 }
2784 true
2785 });
2786
2787 fxg.push(StaticFunctionsXGraphs {
2789 id: generate_uuid_v5(("function", Some(&graph.graphid)), &function_id),
2790 function_id,
2791 graph_id: graph.graphid.clone(),
2792 config: serde_json::Value::Object(serde_json::Map::new()),
2793 });
2794
2795 Ok(())
2796}
2797
2798fn apply_delete_function(
2799 graph: &mut StaticGraph,
2800 params: DeleteFunctionParams,
2801) -> Result<(), MutationError> {
2802 let function_exists = graph
2804 .functions_x_graphs
2805 .as_ref()
2806 .map(|fxgs| fxgs.iter().any(|f| f.id == params.function_mapping_id))
2807 .unwrap_or(false);
2808
2809 if !function_exists {
2810 return Err(MutationError::FunctionNotFound(params.function_mapping_id));
2811 }
2812
2813 if let Some(ref mut fxgs) = graph.functions_x_graphs {
2815 fxgs.retain(|f| f.id != params.function_mapping_id);
2816 }
2817
2818 Ok(())
2819}
2820
2821fn apply_set_descriptor_template(
2822 graph: &mut StaticGraph,
2823 params: SetDescriptorTemplateParams,
2824) -> Result<(), MutationError> {
2825 graph
2826 .set_descriptor_template(¶ms.descriptor_type, ¶ms.string_template)
2827 .map_err(MutationError::Other)
2828}
2829
2830fn apply_delete_node(
2831 graph: &mut StaticGraph,
2832 params: DeleteNodeParams,
2833) -> Result<(), MutationError> {
2834 let node = graph
2836 .find_node_by_alias(¶ms.node_id)
2837 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
2838 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
2839
2840 if node.istopnode {
2842 return Err(MutationError::CannotDeleteRootNode(params.node_id.clone()));
2843 }
2844
2845 let root_id = node.nodeid.clone();
2846
2847 let mut nodes_to_delete = vec![root_id.clone()];
2849 let mut i = 0;
2850 while i < nodes_to_delete.len() {
2851 let current = &nodes_to_delete[i].clone();
2852 for edge in &graph.edges {
2853 if edge.domainnode_id == *current && !nodes_to_delete.contains(&edge.rangenode_id) {
2854 nodes_to_delete.push(edge.rangenode_id.clone());
2855 }
2856 }
2857 i += 1;
2858 }
2859
2860 for nid in &nodes_to_delete {
2862 if let Some(n) = graph.nodes.iter().find(|n| n.nodeid == *nid) {
2863 if n.istopnode {
2864 return Err(MutationError::CannotDeleteRootNode(nid.clone()));
2865 }
2866 }
2867 }
2868
2869 let mut nodegroups_to_delete: Vec<String> = Vec::new();
2871 for nid in &nodes_to_delete {
2872 if let Some(n) = graph.nodes.iter().find(|n| n.nodeid == *nid) {
2873 if let Some(ref ng_id) = n.nodegroup_id {
2874 if *ng_id == n.nodeid && !nodegroups_to_delete.contains(ng_id) {
2875 nodegroups_to_delete.push(ng_id.clone());
2876 }
2877 }
2878 }
2879 }
2880
2881 if let Some(ref mut cxnxws) = graph.cards_x_nodes_x_widgets {
2883 cxnxws.retain(|c| !nodes_to_delete.contains(&c.node_id));
2884 }
2885
2886 graph.edges.retain(|e| {
2888 !nodes_to_delete.contains(&e.domainnode_id) && !nodes_to_delete.contains(&e.rangenode_id)
2889 });
2890
2891 if let Some(ref mut cards) = graph.cards {
2893 cards.retain(|c| !nodegroups_to_delete.contains(&c.nodegroup_id));
2894 }
2895
2896 graph
2898 .nodegroups
2899 .retain(|ng| !nodegroups_to_delete.contains(&ng.nodegroupid));
2900
2901 graph.nodes.retain(|n| !nodes_to_delete.contains(&n.nodeid));
2903
2904 graph.invalidate_indices();
2906
2907 Ok(())
2908}
2909
2910fn apply_delete_nodegroup(
2911 graph: &mut StaticGraph,
2912 params: DeleteNodegroupParams,
2913) -> Result<(), MutationError> {
2914 if !graph
2916 .nodegroups
2917 .iter()
2918 .any(|ng| ng.nodegroupid == params.nodegroup_id)
2919 {
2920 return Err(MutationError::NodegroupNotFound(params.nodegroup_id));
2921 }
2922
2923 let mut nodegroups_to_delete: Vec<String> = vec![params.nodegroup_id.clone()];
2925 let mut i = 0;
2926 while i < nodegroups_to_delete.len() {
2927 let current_ng = nodegroups_to_delete[i].clone();
2928 for ng in &graph.nodegroups {
2930 if ng.parentnodegroup_id.as_ref() == Some(¤t_ng)
2931 && !nodegroups_to_delete.contains(&ng.nodegroupid)
2932 {
2933 nodegroups_to_delete.push(ng.nodegroupid.clone());
2934 }
2935 }
2936 i += 1;
2937 }
2938
2939 let nodes_to_delete: Vec<String> = graph
2941 .nodes
2942 .iter()
2943 .filter(|n| {
2944 n.nodegroup_id
2945 .as_ref()
2946 .map(|ng| nodegroups_to_delete.contains(ng))
2947 .unwrap_or(false)
2948 })
2949 .map(|n| n.nodeid.clone())
2950 .collect();
2951
2952 for node_id in &nodes_to_delete {
2954 if let Some(node) = graph.nodes.iter().find(|n| n.nodeid == *node_id) {
2955 if node.istopnode {
2956 return Err(MutationError::CannotDeleteRootNode(node_id.clone()));
2957 }
2958 }
2959 }
2960
2961 if let Some(ref mut cxnxws) = graph.cards_x_nodes_x_widgets {
2963 cxnxws.retain(|c| !nodes_to_delete.contains(&c.node_id));
2964 }
2965
2966 graph.edges.retain(|e| {
2968 !nodes_to_delete.contains(&e.domainnode_id) && !nodes_to_delete.contains(&e.rangenode_id)
2969 });
2970
2971 if let Some(ref mut cards) = graph.cards {
2973 cards.retain(|c| !nodegroups_to_delete.contains(&c.nodegroup_id));
2974 }
2975
2976 graph
2978 .nodegroups
2979 .retain(|ng| !nodegroups_to_delete.contains(&ng.nodegroupid));
2980
2981 graph.nodes.retain(|n| !nodes_to_delete.contains(&n.nodeid));
2983
2984 graph.invalidate_indices();
2986
2987 Ok(())
2988}
2989
2990fn apply_update_node(
2995 graph: &mut StaticGraph,
2996 params: UpdateNodeParams,
2997 options: &MutatorOptions,
2998) -> Result<(), MutationError> {
2999 let node = graph
3001 .find_node_by_alias(¶ms.node_id)
3002 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
3003 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
3004
3005 let node_id = node.nodeid.clone();
3006
3007 let class_update: Option<Option<Vec<String>>> =
3012 params.ontology_class.map(|v| sanitize_class_list(Some(v)));
3013
3014 if let Some(ref validator) = options.ontology_validator {
3017 if let Some(Some(ref classes)) = class_update {
3018 for c in classes {
3019 if !validator.is_valid_class(c) {
3020 return Err(MutationError::OntologyValidation(
3021 crate::ontology::OntologyValidationDetail::UnknownClass(c.clone()),
3022 ));
3023 }
3024 }
3025 }
3026 }
3027
3028 let node_mut = graph
3030 .nodes
3031 .iter_mut()
3032 .find(|n| n.nodeid == node_id)
3033 .ok_or_else(|| MutationError::NodeNotFound(node_id.clone()))?;
3034
3035 if let Some(name) = params.name {
3037 node_mut.name = name;
3038 }
3039 if let Some(new_classes) = class_update {
3040 node_mut.ontologyclass = new_classes;
3041 }
3042 if let Some(parent_property) = params.parent_property {
3043 node_mut.parentproperty = if parent_property.is_empty() {
3044 None
3045 } else {
3046 Some(parent_property)
3047 };
3048 }
3049 if let Some(description) = params.description {
3050 node_mut.description = Some(StaticTranslatableString::from_string(&description));
3051 }
3052 if let Some(serde_json::Value::Object(map)) = params.config {
3053 for (k, v) in map {
3055 node_mut.config.insert(k, v);
3056 }
3057 }
3058
3059 if let Some(exportable) = params.options.exportable {
3061 node_mut.exportable = exportable;
3062 }
3063 if let Some(fieldname) = params.options.fieldname {
3064 node_mut.fieldname = if fieldname.is_empty() {
3065 None
3066 } else {
3067 Some(fieldname)
3068 };
3069 }
3070 if let Some(isrequired) = params.options.isrequired {
3071 node_mut.isrequired = isrequired;
3072 }
3073 if let Some(issearchable) = params.options.issearchable {
3074 node_mut.issearchable = issearchable;
3075 }
3076 if let Some(sortorder) = params.options.sortorder {
3077 node_mut.sortorder = Some(sortorder);
3078 }
3079
3080 Ok(())
3081}
3082
3083fn apply_change_node_type(
3084 graph: &mut StaticGraph,
3085 params: ChangeNodeTypeParams,
3086) -> Result<(), MutationError> {
3087 let node = graph
3089 .find_node_by_alias(¶ms.node_id)
3090 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
3091 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
3092
3093 let node_id = node.nodeid.clone();
3094
3095 let has_widgets = graph
3097 .cards_x_nodes_x_widgets
3098 .as_ref()
3099 .map(|cxnxws| cxnxws.iter().any(|c| c.node_id == node_id))
3100 .unwrap_or(false);
3101
3102 if has_widgets {
3103 return Err(MutationError::NodeHasDependentWidgets(params.node_id));
3104 }
3105
3106 let node_mut = graph
3108 .nodes
3109 .iter_mut()
3110 .find(|n| n.nodeid == node_id)
3111 .ok_or_else(|| MutationError::NodeNotFound(node_id.clone()))?;
3112
3113 node_mut.datatype = params.datatype;
3115
3116 if let Some(name) = params.name {
3118 node_mut.name = name;
3119 }
3120 if let Some(classes) = params.ontology_class {
3121 node_mut.ontologyclass = sanitize_class_list(Some(classes));
3122 }
3123 if let Some(parent_property) = params.parent_property {
3124 node_mut.parentproperty = if parent_property.is_empty() {
3125 None
3126 } else {
3127 Some(parent_property)
3128 };
3129 }
3130 if let Some(description) = params.description {
3131 node_mut.description = Some(StaticTranslatableString::from_string(&description));
3132 }
3133 if let Some(serde_json::Value::Object(map)) = params.config {
3134 for (k, v) in map {
3136 node_mut.config.insert(k, v);
3137 }
3138 }
3139
3140 if let Some(exportable) = params.options.exportable {
3142 node_mut.exportable = exportable;
3143 }
3144 if let Some(fieldname) = params.options.fieldname {
3145 node_mut.fieldname = if fieldname.is_empty() {
3146 None
3147 } else {
3148 Some(fieldname)
3149 };
3150 }
3151 if let Some(isrequired) = params.options.isrequired {
3152 node_mut.isrequired = isrequired;
3153 }
3154 if let Some(issearchable) = params.options.issearchable {
3155 node_mut.issearchable = issearchable;
3156 }
3157 if let Some(sortorder) = params.options.sortorder {
3158 node_mut.sortorder = Some(sortorder);
3159 }
3160
3161 Ok(())
3162}
3163
3164fn apply_change_cardinality(
3165 graph: &mut StaticGraph,
3166 params: ChangeCardinalityParams,
3167) -> Result<(), MutationError> {
3168 let (node_id, nodegroup_id) = {
3170 let node = graph
3171 .find_node_by_alias(¶ms.node_id)
3172 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
3173 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
3174
3175 let nodegroup_id = node.nodegroup_id.clone().ok_or_else(|| {
3176 MutationError::Other(format!(
3177 "Node '{}' has no nodegroup_id - cannot change cardinality",
3178 params.node_id
3179 ))
3180 })?;
3181
3182 (node.nodeid.clone(), nodegroup_id)
3183 };
3184
3185 let nodegroup = graph
3191 .nodegroups
3192 .iter()
3193 .find(|ng| ng.nodegroupid == nodegroup_id)
3194 .ok_or_else(|| MutationError::NodegroupNotFound(nodegroup_id.clone()))?;
3195
3196 let is_grouping_node = match &nodegroup.grouping_node_id {
3197 Some(grouping_id) => grouping_id == &node_id,
3198 None => {
3199 nodegroup_id == node_id
3202 }
3203 };
3204
3205 if !is_grouping_node {
3206 return Err(MutationError::Other(format!(
3207 "Node '{}' is not the grouping node for nodegroup '{}'. Only the grouping node can change cardinality.",
3208 params.node_id, nodegroup_id
3209 )));
3210 }
3211
3212 let nodegroup_mut = graph
3214 .nodegroups
3215 .iter_mut()
3216 .find(|ng| ng.nodegroupid == nodegroup_id)
3217 .ok_or_else(|| MutationError::NodegroupNotFound(nodegroup_id.clone()))?;
3218
3219 nodegroup_mut.cardinality = Some(params.cardinality.as_str().to_string());
3220
3221 Ok(())
3222}
3223
3224fn apply_rename_node(
3225 graph: &mut StaticGraph,
3226 params: RenameNodeParams,
3227) -> Result<(), MutationError> {
3228 let node = graph
3230 .find_node_by_alias(¶ms.node_id)
3231 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_id))
3232 .ok_or_else(|| MutationError::NodeNotFound(params.node_id.clone()))?;
3233
3234 let node_id = node.nodeid.clone();
3235
3236 if let Some(ref new_alias) = params.alias {
3238 let alias_exists = graph
3240 .nodes
3241 .iter()
3242 .any(|n| n.nodeid != node_id && n.alias.as_ref() == Some(new_alias));
3243 if alias_exists {
3244 return Err(MutationError::AliasAlreadyExists(new_alias.clone()));
3245 }
3246 }
3247
3248 let node_mut = graph
3250 .nodes
3251 .iter_mut()
3252 .find(|n| n.nodeid == node_id)
3253 .ok_or_else(|| MutationError::NodeNotFound(node_id.clone()))?;
3254
3255 if let Some(alias) = params.alias {
3256 node_mut.alias = if alias.is_empty() { None } else { Some(alias) };
3257 }
3258 let name_changed = params.name.is_some();
3259 if let Some(name) = params.name {
3260 node_mut.name = name;
3261 }
3262 if let Some(description) = params.description {
3263 node_mut.description = Some(StaticTranslatableString::from_string(&description));
3264 }
3265
3266 if name_changed && params.realign_card {
3268 let _ = apply_realign_card_from_node(
3269 graph,
3270 RealignCardFromNodeParams {
3271 node_alias: node_id,
3272 },
3273 );
3274 }
3275
3276 Ok(())
3277}
3278
3279fn apply_rename_card(
3280 graph: &mut StaticGraph,
3281 params: RenameCardParams,
3282) -> Result<(), MutationError> {
3283 let lang = params.language.unwrap_or_else(|| "en".to_string());
3284
3285 let card_mut = graph
3287 .cards
3288 .as_mut()
3289 .and_then(|cards| {
3290 cards
3291 .iter_mut()
3292 .find(|c| c.cardid == params.card_id || c.nodegroup_id == params.card_id)
3293 })
3294 .ok_or(MutationError::CardNotFound(params.card_id))?;
3295
3296 if let Some(name_i18n) = params.name_i18n {
3298 card_mut.name = StaticTranslatableString::from_translations(name_i18n, Some(lang.clone()));
3299 } else if let Some(name) = params.name {
3300 card_mut.name.translations.insert(lang.clone(), name);
3301 }
3302
3303 if let Some(desc_i18n) = params.description_i18n {
3305 card_mut.description = Some(StaticTranslatableString::from_translations(
3306 desc_i18n,
3307 Some(lang),
3308 ));
3309 } else if let Some(desc) = params.description {
3310 let description = card_mut
3311 .description
3312 .get_or_insert_with(StaticTranslatableString::empty);
3313 description.translations.insert(lang, desc);
3314 }
3315
3316 Ok(())
3317}
3318
3319fn apply_realign_card_from_node(
3320 graph: &mut StaticGraph,
3321 params: RealignCardFromNodeParams,
3322) -> Result<(), MutationError> {
3323 let node = graph
3325 .find_node_by_alias(¶ms.node_alias)
3326 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == params.node_alias))
3327 .ok_or_else(|| MutationError::NodeNotFound(params.node_alias.clone()))?;
3328
3329 let node_name = node.name.clone();
3330 let node_id = node.nodeid.clone();
3331 let nodegroup_id = node
3332 .nodegroup_id
3333 .clone()
3334 .ok_or_else(|| MutationError::NodegroupNotFound(params.node_alias.clone()))?;
3335
3336 if let Some(cards) = graph.cards.as_mut() {
3338 if let Some(card) = cards.iter_mut().find(|c| c.nodegroup_id == nodegroup_id) {
3339 card.name = StaticTranslatableString::from_string(&node_name);
3340 }
3341 }
3342
3343 if let Some(cxnxws) = graph.cards_x_nodes_x_widgets.as_mut() {
3345 for cxnxw in cxnxws.iter_mut().filter(|c| c.node_id == node_id) {
3346 cxnxw.label = StaticTranslatableString::from_string(&node_name);
3347 if let serde_json::Value::Object(ref mut map) = cxnxw.config {
3349 map.insert(
3350 "label".to_string(),
3351 serde_json::Value::String(node_name.clone()),
3352 );
3353 }
3354 }
3355 }
3356
3357 Ok(())
3358}
3359
3360fn apply_rename_graph(
3361 graph: &mut StaticGraph,
3362 params: RenameGraphParams,
3363) -> Result<(), MutationError> {
3364 if let Some(name_map) = params.name {
3366 let new_name = StaticTranslatableString::from_translations(name_map, None);
3367
3368 graph.name = new_name.clone();
3370
3371 let root_display_name = new_name.to_string_default();
3373 graph.root.name = root_display_name.clone();
3374
3375 let new_slug = slugify(&root_display_name);
3377 graph.slug = Some(new_slug.clone());
3378 graph.root.alias = Some(new_slug.clone());
3379
3380 if let Some(root_node) = graph.nodes.iter_mut().find(|n| n.istopnode) {
3382 root_node.name = root_display_name;
3383 root_node.alias = Some(new_slug);
3384 }
3385 }
3386
3387 if let Some(desc_map) = params.description {
3389 graph.description = Some(StaticTranslatableString::from_translations(desc_map, None));
3390 }
3391
3392 if let Some(subtitle_map) = params.subtitle {
3394 graph.subtitle = Some(StaticTranslatableString::from_translations(
3395 subtitle_map,
3396 None,
3397 ));
3398 }
3399
3400 if let Some(author) = params.author {
3402 graph.author = if author.is_empty() {
3403 None
3404 } else {
3405 Some(author)
3406 };
3407 }
3408
3409 Ok(())
3410}
3411
3412fn apply_coppice_subgraph(
3421 graph: &mut StaticGraph,
3422 params: CoppiceSubgraphParams,
3423) -> Result<(), MutationError> {
3424 let root_nodeid = graph
3426 .nodes
3427 .iter()
3428 .find(|n| n.alias.as_deref() == Some(¶ms.subject))
3429 .map(|n| n.nodeid.clone())
3430 .ok_or_else(|| {
3431 MutationError::NodeNotFound(format!(
3432 "coppice_subgraph: node with alias '{}' not found",
3433 params.subject
3434 ))
3435 })?;
3436
3437 let mut children: HashMap<String, Vec<String>> = HashMap::new();
3439 for edge in &graph.edges {
3440 children
3441 .entry(edge.domainnode_id.clone())
3442 .or_default()
3443 .push(edge.rangenode_id.clone());
3444 }
3445
3446 let mut queue = std::collections::VecDeque::new();
3448 queue.push_back(root_nodeid);
3449
3450 while let Some(nid) = queue.pop_front() {
3451 if let Some(node) = graph.nodes.iter_mut().find(|n| n.nodeid == nid) {
3452 let existing = node.sourcebranchpublication_id.as_deref();
3453 if existing.is_some() && existing != Some(¶ms.publication_id) {
3454 continue;
3456 }
3457 node.sourcebranchpublication_id = Some(params.publication_id.clone());
3458 }
3459 if let Some(child_ids) = children.get(&nid) {
3460 for child_id in child_ids {
3461 queue.push_back(child_id.clone());
3462 }
3463 }
3464 }
3465
3466 Ok(())
3467}
3468
3469struct IdRemapper {
3475 graph_id: String,
3477 suffix: String,
3479 branch_publication_id: Option<String>,
3481 node_map: HashMap<String, String>,
3483 nodegroup_map: HashMap<String, String>,
3485 edge_map: HashMap<String, String>,
3487 card_map: HashMap<String, String>,
3489 cxnxw_map: HashMap<String, String>,
3491 constraint_map: HashMap<String, String>,
3493 alias_map: HashMap<String, String>,
3495}
3496
3497impl IdRemapper {
3498 fn new(graph_id: &str, suffix: Option<&str>, branch_publication_id: Option<String>) -> Self {
3499 Self {
3500 graph_id: graph_id.to_string(),
3501 suffix: suffix.unwrap_or("").to_string(),
3502 branch_publication_id,
3503 node_map: HashMap::new(),
3504 nodegroup_map: HashMap::new(),
3505 edge_map: HashMap::new(),
3506 card_map: HashMap::new(),
3507 cxnxw_map: HashMap::new(),
3508 constraint_map: HashMap::new(),
3509 alias_map: HashMap::new(),
3510 }
3511 }
3512
3513 fn remap_node(&mut self, old_id: &str) -> String {
3515 let new_id = generate_uuid_v5(
3516 ("graph", Some(&self.graph_id)),
3517 &format!("subgraph-node-{}-{}", old_id, self.suffix),
3518 );
3519 self.node_map.insert(old_id.to_string(), new_id.clone());
3520 new_id
3521 }
3522
3523 fn remap_nodegroup(&mut self, old_id: &str) -> String {
3525 let new_id = generate_uuid_v5(
3526 ("graph", Some(&self.graph_id)),
3527 &format!("subgraph-ng-{}-{}", old_id, self.suffix),
3528 );
3529 self.nodegroup_map
3530 .insert(old_id.to_string(), new_id.clone());
3531 new_id
3532 }
3533
3534 fn remap_edge(&mut self, old_id: &str) -> String {
3536 let new_id = generate_uuid_v5(
3537 ("graph", Some(&self.graph_id)),
3538 &format!("subgraph-edge-{}-{}", old_id, self.suffix),
3539 );
3540 self.edge_map.insert(old_id.to_string(), new_id.clone());
3541 new_id
3542 }
3543
3544 fn remap_card(&mut self, old_id: &str) -> String {
3546 let new_id = generate_uuid_v5(
3547 ("graph", Some(&self.graph_id)),
3548 &format!("subgraph-card-{}-{}", old_id, self.suffix),
3549 );
3550 self.card_map.insert(old_id.to_string(), new_id.clone());
3551 new_id
3552 }
3553
3554 fn remap_cxnxw(&mut self, old_id: &str) -> String {
3556 let new_id = generate_uuid_v5(
3557 ("graph", Some(&self.graph_id)),
3558 &format!("subgraph-cxnxw-{}-{}", old_id, self.suffix),
3559 );
3560 self.cxnxw_map.insert(old_id.to_string(), new_id.clone());
3561 new_id
3562 }
3563
3564 fn remap_constraint(&mut self, old_id: &str) -> String {
3566 let new_id = generate_uuid_v5(
3567 ("graph", Some(&self.graph_id)),
3568 &format!("subgraph-constraint-{}-{}", old_id, self.suffix),
3569 );
3570 self.constraint_map
3571 .insert(old_id.to_string(), new_id.clone());
3572 new_id
3573 }
3574
3575 fn get_node(&self, old_id: &str) -> Option<&String> {
3577 self.node_map.get(old_id)
3578 }
3579
3580 fn get_nodegroup(&self, old_id: &str) -> Option<&String> {
3582 self.nodegroup_map.get(old_id)
3583 }
3584
3585 fn get_card(&self, old_id: &str) -> Option<&String> {
3587 self.card_map.get(old_id)
3588 }
3589
3590 fn register_alias(&mut self, old_alias: &str, new_alias: String) {
3592 self.alias_map.insert(old_alias.to_string(), new_alias);
3593 }
3594
3595 fn get_alias(&self, alias: Option<&str>) -> Option<String> {
3597 alias.map(|a| {
3598 self.alias_map
3599 .get(a)
3600 .cloned()
3601 .unwrap_or_else(|| a.to_string())
3602 })
3603 }
3604}
3605
3606fn make_name_unique(name: &str, existing: &HashSet<String>) -> String {
3608 if !existing.contains(name) {
3609 return name.to_string();
3610 }
3611
3612 let mut counter = 1;
3613 loop {
3614 let candidate = format!("{}_n{}", name, counter);
3615 if !existing.contains(&candidate) {
3616 return candidate;
3617 }
3618 counter += 1;
3619 }
3620}
3621
3622fn apply_add_subgraph(
3624 graph: &mut StaticGraph,
3625 params: AddSubgraphParams,
3626) -> Result<(), MutationError> {
3627 let subgraph = params.subgraph;
3628 let target_node_id = params.target_node_id;
3629 let ontology_property = params.ontology_property;
3630 let alias_suffix = params.alias_suffix;
3631
3632 let branch_publication_id = subgraph
3636 .publication
3637 .as_ref()
3638 .and_then(|p| p.get("publicationid"))
3639 .and_then(|v| v.as_str())
3640 .map(|s| s.to_string())
3641 .ok_or_else(|| MutationError::InvalidSubgraph(format!(
3642 "Subgraph '{}' has no publication.publicationid — the branch must be published before it can be added as a subgraph",
3643 subgraph.graphid
3644 )))?;
3645
3646 let target_node = graph
3648 .find_node_by_alias(&target_node_id)
3649 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == target_node_id))
3650 .ok_or_else(|| MutationError::NodeNotFound(target_node_id.clone()))?;
3651 let target_node_id = target_node.nodeid.clone();
3652 let target_nodegroup_id = target_node.nodegroup_id.clone();
3653
3654 let root_node = subgraph
3656 .nodes
3657 .iter()
3658 .find(|n| n.istopnode)
3659 .or(Some(&subgraph.root))
3660 .ok_or(MutationError::BranchHasNoRoot)?;
3661 let root_node_id = root_node.nodeid.clone();
3662 let root_nodegroup_id = root_node
3663 .nodegroup_id
3664 .clone()
3665 .unwrap_or_else(|| root_node_id.clone());
3666
3667 let mut existing_aliases: HashSet<String> =
3670 graph.nodes.iter().filter_map(|n| n.alias.clone()).collect();
3671
3672 let suffix_ref = alias_suffix.as_deref();
3674 let mut remapper = IdRemapper::new(&graph.graphid, suffix_ref, Some(branch_publication_id));
3675
3676 for node in &subgraph.nodes {
3679 if node.nodeid == root_node_id {
3680 continue; }
3682 if let Some(ref alias) = node.alias {
3683 let prefixed_alias = if let Some(ref prefix) = params.alias_prefix {
3684 format!("{}_{}", prefix, alias)
3685 } else {
3686 alias.clone()
3687 };
3688 let new_alias = make_name_unique(&prefixed_alias, &existing_aliases);
3689 if new_alias != *alias {
3690 remapper.register_alias(alias, new_alias.clone());
3692 }
3693 existing_aliases.insert(new_alias);
3695 }
3696 }
3697
3698 for node in &subgraph.nodes {
3702 remapper.remap_node(&node.nodeid);
3703 }
3704
3705 for nodegroup in &subgraph.nodegroups {
3710 if nodegroup.nodegroupid != root_nodegroup_id {
3711 if let Some(node_id) = remapper.get_node(&nodegroup.nodegroupid) {
3712 let node_id = node_id.clone();
3714 remapper
3715 .nodegroup_map
3716 .insert(nodegroup.nodegroupid.clone(), node_id);
3717 } else {
3718 remapper.remap_nodegroup(&nodegroup.nodegroupid);
3719 }
3720 }
3721 }
3722
3723 for edge in &subgraph.edges {
3725 remapper.remap_edge(&edge.edgeid);
3726 }
3727
3728 if let Some(ref cards) = subgraph.cards {
3731 for card in cards {
3732 remapper.remap_card(&card.cardid);
3733 }
3734 }
3735
3736 if let Some(ref cxnxws) = subgraph.cards_x_nodes_x_widgets {
3738 for cxnxw in cxnxws {
3739 remapper.remap_cxnxw(&cxnxw.id);
3740 }
3741 }
3742
3743 for node in subgraph.nodes {
3749 let is_branch_root = node.nodeid == root_node_id;
3750
3751 let new_node_id = remapper
3752 .get_node(&node.nodeid)
3753 .ok_or_else(|| {
3754 MutationError::InvalidSubgraph(format!("Node {} not mapped", node.nodeid))
3755 })?
3756 .clone();
3757
3758 let new_nodegroup_id = if is_branch_root {
3760 Some(new_node_id.clone())
3762 } else {
3763 node.nodegroup_id.as_ref().and_then(|ng_id| {
3764 if *ng_id == root_nodegroup_id {
3765 remapper.get_node(&root_node_id).cloned()
3767 } else {
3768 remapper.get_nodegroup(ng_id).cloned()
3769 }
3770 })
3771 };
3772
3773 let prefixed_name = if let Some(ref prefix) = params.name_prefix {
3774 format!("{} {}", prefix, node.name)
3775 } else {
3776 node.name
3777 };
3778
3779 let new_node = StaticNode {
3780 nodeid: new_node_id,
3781 name: prefixed_name,
3782 alias: remapper.get_alias(node.alias.as_deref()),
3783 datatype: node.datatype,
3784 nodegroup_id: new_nodegroup_id,
3785 graph_id: graph.graphid.clone(),
3786 is_collector: node.is_collector,
3787 isrequired: node.isrequired,
3788 exportable: node.exportable,
3789 sortorder: node.sortorder,
3790 config: node.config,
3791 parentproperty: node.parentproperty,
3792 ontologyclass: node.ontologyclass,
3793 description: node.description,
3794 fieldname: node.fieldname,
3795 hascustomalias: node.hascustomalias,
3796 issearchable: node.issearchable,
3797 istopnode: false, sourcebranchpublication_id: remapper.branch_publication_id.clone(),
3800 source_identifier_id: node.source_identifier_id,
3801 is_immutable: node.is_immutable,
3802 };
3803 graph.push_node(new_node);
3804 }
3805
3806 for nodegroup in subgraph.nodegroups {
3810 if nodegroup.nodegroupid == root_nodegroup_id {
3811 let new_root_id = remapper
3812 .get_node(&root_node_id)
3813 .ok_or_else(|| {
3814 MutationError::InvalidSubgraph("Branch root not mapped".to_string())
3815 })?
3816 .clone();
3817 let new_ng = StaticNodegroup {
3818 nodegroupid: new_root_id.clone(),
3819 cardinality: nodegroup.cardinality.clone(),
3820 parentnodegroup_id: target_nodegroup_id.clone(),
3821 legacygroupid: nodegroup.legacygroupid.clone(),
3822 grouping_node_id: Some(new_root_id),
3823 };
3824 graph.push_nodegroup(new_ng);
3825 continue;
3826 }
3827
3828 let new_ng_id = remapper
3829 .get_nodegroup(&nodegroup.nodegroupid)
3830 .ok_or_else(|| {
3831 MutationError::InvalidSubgraph(format!(
3832 "Nodegroup {} not mapped",
3833 nodegroup.nodegroupid
3834 ))
3835 })?
3836 .clone();
3837
3838 let new_parent_ng_id = nodegroup.parentnodegroup_id.as_ref().and_then(|parent_id| {
3840 if *parent_id == root_nodegroup_id {
3841 remapper.get_node(&root_node_id).cloned()
3843 } else {
3844 remapper.get_nodegroup(parent_id).cloned()
3845 }
3846 });
3847
3848 let new_grouping_node_id = nodegroup
3850 .grouping_node_id
3851 .as_ref()
3852 .and_then(|gn_id| remapper.get_node(gn_id).cloned());
3853
3854 let new_nodegroup = StaticNodegroup {
3855 nodegroupid: new_ng_id,
3856 cardinality: nodegroup.cardinality,
3857 parentnodegroup_id: new_parent_ng_id,
3858 legacygroupid: nodegroup.legacygroupid,
3859 grouping_node_id: new_grouping_node_id,
3860 };
3861 graph.push_nodegroup(new_nodegroup);
3862 }
3863
3864 {
3868 let new_root_id = remapper
3869 .get_node(&root_node_id)
3870 .ok_or_else(|| MutationError::InvalidSubgraph("Branch root not mapped".to_string()))?
3871 .clone();
3872 let connect_edge_id = generate_uuid_v5(
3873 ("graph", Some(&graph.graphid)),
3874 &format!(
3875 "subgraph-connect-{}-{}-{}",
3876 target_node_id, new_root_id, remapper.suffix
3877 ),
3878 );
3879 let connect_edge = StaticEdge {
3880 edgeid: connect_edge_id,
3881 domainnode_id: target_node_id.clone(),
3882 rangenode_id: new_root_id,
3883 graph_id: graph.graphid.clone(),
3884 name: None,
3885 ontologyproperty: if ontology_property.is_empty() {
3886 None
3887 } else {
3888 Some(ontology_property.clone())
3889 },
3890 description: None,
3891 source_identifier_id: None,
3892 };
3893 graph.push_edge(connect_edge);
3894 }
3895 for edge in subgraph.edges {
3896 {
3897 let new_edge_id = remapper
3899 .edge_map
3900 .get(&edge.edgeid)
3901 .ok_or_else(|| {
3902 MutationError::InvalidSubgraph(format!("Edge {} not mapped", edge.edgeid))
3903 })?
3904 .clone();
3905
3906 let new_domain = remapper
3907 .get_node(&edge.domainnode_id)
3908 .ok_or_else(|| {
3909 MutationError::InvalidSubgraph(format!(
3910 "Domain node {} not mapped",
3911 edge.domainnode_id
3912 ))
3913 })?
3914 .clone();
3915
3916 let new_range = remapper
3917 .get_node(&edge.rangenode_id)
3918 .ok_or_else(|| {
3919 MutationError::InvalidSubgraph(format!(
3920 "Range node {} not mapped",
3921 edge.rangenode_id
3922 ))
3923 })?
3924 .clone();
3925
3926 let new_edge = StaticEdge {
3927 edgeid: new_edge_id,
3928 domainnode_id: new_domain,
3929 rangenode_id: new_range,
3930 graph_id: graph.graphid.clone(),
3931 name: edge.name,
3932 ontologyproperty: edge.ontologyproperty,
3933 description: edge.description,
3934 source_identifier_id: None,
3935 };
3936 graph.push_edge(new_edge);
3937 }
3938 }
3939
3940 if let Some(cards) = subgraph.cards {
3942 for card in cards {
3943 let new_card_id = remapper
3944 .get_card(&card.cardid)
3945 .ok_or_else(|| {
3946 MutationError::InvalidSubgraph(format!("Card {} not mapped", card.cardid))
3947 })?
3948 .clone();
3949
3950 let new_ng_id = if card.nodegroup_id == root_nodegroup_id {
3951 remapper
3953 .get_node(&root_node_id)
3954 .ok_or_else(|| {
3955 MutationError::InvalidSubgraph("Branch root not mapped".to_string())
3956 })?
3957 .clone()
3958 } else {
3959 remapper
3960 .get_nodegroup(&card.nodegroup_id)
3961 .ok_or_else(|| {
3962 MutationError::InvalidSubgraph(format!(
3963 "Card nodegroup {} not mapped",
3964 card.nodegroup_id
3965 ))
3966 })?
3967 .clone()
3968 };
3969
3970 let new_constraints: Vec<_> = card
3972 .constraints
3973 .into_iter()
3974 .map(|c| {
3975 let new_constraint_id = remapper.remap_constraint(&c.constraintid);
3976 let new_nodes: Vec<_> = c
3977 .nodes
3978 .into_iter()
3979 .filter_map(|n| remapper.get_node(&n).cloned())
3980 .collect();
3981 crate::graph::StaticConstraint {
3982 card_id: new_card_id.clone(),
3983 constraintid: new_constraint_id,
3984 nodes: new_nodes,
3985 uniquetoallinstances: c.uniquetoallinstances,
3986 }
3987 })
3988 .collect();
3989
3990 let new_card = StaticCard {
3991 active: card.active,
3992 cardid: new_card_id,
3993 component_id: card.component_id, config: card.config,
3995 constraints: new_constraints,
3996 cssclass: card.cssclass,
3997 description: card.description,
3998 graph_id: graph.graphid.clone(),
3999 helpenabled: card.helpenabled,
4000 helptext: card.helptext,
4001 helptitle: card.helptitle,
4002 instructions: card.instructions,
4003 is_editable: card.is_editable,
4004 name: card.name,
4005 nodegroup_id: new_ng_id,
4006 sortorder: Some(card.sortorder.unwrap_or(0)),
4007 visible: card.visible,
4008 source_identifier_id: None,
4009 };
4010 graph.push_card(new_card);
4011 }
4012 }
4013
4014 if let Some(cxnxws) = subgraph.cards_x_nodes_x_widgets {
4016 for cxnxw in cxnxws {
4017 let new_id = remapper
4018 .cxnxw_map
4019 .get(&cxnxw.id)
4020 .ok_or_else(|| {
4021 MutationError::InvalidSubgraph(format!("CXNXW {} not mapped", cxnxw.id))
4022 })?
4023 .clone();
4024
4025 let new_card_id = remapper
4026 .get_card(&cxnxw.card_id)
4027 .ok_or_else(|| {
4028 MutationError::InvalidSubgraph(format!(
4029 "CXNXW card {} not mapped",
4030 cxnxw.card_id
4031 ))
4032 })?
4033 .clone();
4034
4035 let new_node_id = remapper
4036 .get_node(&cxnxw.node_id)
4037 .ok_or_else(|| {
4038 MutationError::InvalidSubgraph(format!(
4039 "CXNXW node {} not mapped",
4040 cxnxw.node_id
4041 ))
4042 })?
4043 .clone();
4044
4045 let new_cxnxw = StaticCardsXNodesXWidgets {
4046 id: new_id,
4047 card_id: new_card_id,
4048 node_id: new_node_id,
4049 widget_id: cxnxw.widget_id, config: cxnxw.config,
4051 label: cxnxw.label,
4052 sortorder: Some(cxnxw.sortorder.unwrap_or(0)),
4053 visible: cxnxw.visible,
4054 source_identifier_id: None,
4055 };
4056 graph.push_card_x_node_x_widget(new_cxnxw);
4057 }
4058 }
4059
4060 Ok(())
4061}
4062
4063fn apply_update_subgraph(
4068 graph: &mut StaticGraph,
4069 params: UpdateSubgraphParams,
4070) -> Result<(), MutationError> {
4071 let subgraph = params.subgraph;
4072 let target_node_id = params.target_node_id.clone();
4073 let ontology_property = params.ontology_property;
4074 let remove_orphaned = params.remove_orphaned;
4075
4076 let branch_publication_id = subgraph
4078 .publication
4079 .as_ref()
4080 .and_then(|p| p.get("publicationid"))
4081 .and_then(|v| v.as_str())
4082 .map(|s| s.to_string())
4083 .ok_or_else(|| MutationError::InvalidSubgraph(format!(
4084 "Subgraph '{}' has no publication.publicationid — the branch must be published before it can be added as a subgraph",
4085 subgraph.graphid
4086 )))?;
4087
4088 let target_node_ref = graph
4090 .find_node_by_alias(&target_node_id)
4091 .or_else(|| graph.nodes.iter().find(|n| n.nodeid == target_node_id))
4092 .ok_or_else(|| MutationError::NodeNotFound(target_node_id.clone()))?;
4093 let target_node_id = target_node_ref.nodeid.clone();
4094
4095 let root_node = subgraph
4097 .nodes
4098 .iter()
4099 .find(|n| n.istopnode)
4100 .or(Some(&subgraph.root))
4101 .ok_or(MutationError::BranchHasNoRoot)?;
4102 let root_node_id = root_node.nodeid.clone();
4103
4104 let existing_branch_nodes =
4107 find_branch_nodes_by_traversal(graph, &target_node_id, &branch_publication_id)?;
4108
4109 if existing_branch_nodes.is_empty() {
4111 return apply_add_subgraph(
4113 graph,
4114 AddSubgraphParams {
4115 subgraph,
4116 target_node_id: params.target_node_id,
4117 ontology_property,
4118 alias_suffix: params.alias_suffix,
4119 alias_prefix: params.alias_prefix,
4120 name_prefix: params.name_prefix,
4121 },
4122 );
4123 }
4124
4125 let existing_by_alias: HashMap<String, String> = existing_branch_nodes
4128 .iter()
4129 .filter_map(|(node_id, alias)| alias.clone().map(|a| (a, node_id.clone())))
4130 .collect();
4131
4132 let new_branch_aliases: HashSet<String> = subgraph
4134 .nodes
4135 .iter()
4136 .filter(|n| n.nodeid != root_node_id)
4137 .filter_map(|n| n.alias.clone())
4138 .collect();
4139
4140 let mut nodes_to_update: Vec<(&StaticNode, String)> = Vec::new(); let mut nodes_to_add: Vec<&StaticNode> = Vec::new();
4147
4148 for node in &subgraph.nodes {
4149 if node.nodeid == root_node_id {
4150 continue; }
4152 if let Some(ref alias) = node.alias {
4153 let lookup_alias = if let Some(ref prefix) = params.alias_prefix {
4156 format!("{}_{}", prefix, alias)
4157 } else {
4158 alias.clone()
4159 };
4160 if let Some(existing_node_id) = existing_by_alias.get(&lookup_alias) {
4161 nodes_to_update.push((node, existing_node_id.clone()));
4162 } else {
4163 nodes_to_add.push(node);
4164 }
4165 } else {
4166 nodes_to_add.push(node);
4168 }
4169 }
4170
4171 let expected_existing_aliases: HashSet<String> = if let Some(ref prefix) = params.alias_prefix {
4173 new_branch_aliases
4174 .iter()
4175 .map(|a| format!("{}_{}", prefix, a))
4176 .collect()
4177 } else {
4178 new_branch_aliases.clone()
4179 };
4180
4181 let orphaned_node_ids: HashSet<String> = if remove_orphaned {
4182 existing_branch_nodes
4183 .iter()
4184 .filter(|(_, alias)| {
4185 alias
4186 .as_ref()
4187 .map(|a| !expected_existing_aliases.contains(a))
4188 .unwrap_or(true)
4189 })
4190 .map(|(node_id, _)| node_id.clone())
4191 .collect()
4192 } else {
4193 HashSet::new()
4194 };
4195
4196 for (new_node, existing_node_id) in nodes_to_update {
4198 if let Some(existing) = graph
4200 .nodes
4201 .iter_mut()
4202 .find(|n| n.nodeid == existing_node_id)
4203 {
4204 existing.name = if let Some(ref prefix) = params.name_prefix {
4206 format!("{} {}", prefix, new_node.name)
4207 } else {
4208 new_node.name.clone()
4209 };
4210 existing.datatype = new_node.datatype.clone();
4211 existing.ontologyclass = new_node.ontologyclass.clone();
4212 existing.config = new_node.config.clone();
4213 existing.description = new_node.description.clone();
4214 existing.isrequired = new_node.isrequired;
4215 existing.issearchable = new_node.issearchable;
4216 existing.exportable = new_node.exportable;
4217 existing.sortorder = new_node.sortorder;
4218 existing.is_collector = new_node.is_collector;
4219 existing.is_immutable = new_node.is_immutable;
4220 }
4222 }
4223
4224 if !nodes_to_add.is_empty() {
4226 let target_node = graph
4228 .nodes
4229 .iter()
4230 .find(|n| n.nodeid == target_node_id)
4231 .ok_or_else(|| MutationError::NodeNotFound(target_node_id.clone()))?;
4232 let target_nodegroup_id = target_node.nodegroup_id.clone();
4233
4234 let root_nodegroup_id = root_node
4236 .nodegroup_id
4237 .clone()
4238 .unwrap_or_else(|| root_node_id.clone());
4239
4240 let mut existing_aliases: HashSet<String> =
4242 graph.nodes.iter().filter_map(|n| n.alias.clone()).collect();
4243
4244 let suffix_ref = params.alias_suffix.as_deref();
4245 let mut remapper = IdRemapper::new(
4246 &graph.graphid,
4247 suffix_ref,
4248 Some(branch_publication_id.clone()),
4249 );
4250
4251 for node in &nodes_to_add {
4253 if let Some(ref alias) = node.alias {
4254 let prefixed_alias = if let Some(ref prefix) = params.alias_prefix {
4255 format!("{}_{}", prefix, alias)
4256 } else {
4257 alias.clone()
4258 };
4259 let new_alias = make_name_unique(&prefixed_alias, &existing_aliases);
4260 if new_alias != *alias {
4261 remapper.register_alias(alias, new_alias.clone());
4262 }
4263 existing_aliases.insert(new_alias);
4264 }
4265 }
4266
4267 for node in &nodes_to_add {
4269 remapper.remap_node(&node.nodeid);
4270 }
4271
4272 let new_node_nodegroups: HashSet<String> = nodes_to_add
4274 .iter()
4275 .filter_map(|n| n.nodegroup_id.clone())
4276 .filter(|ng_id| *ng_id != root_nodegroup_id)
4277 .collect();
4278
4279 for nodegroup in &subgraph.nodegroups {
4280 if new_node_nodegroups.contains(&nodegroup.nodegroupid) {
4281 remapper.remap_nodegroup(&nodegroup.nodegroupid);
4282 }
4283 }
4284
4285 for node in nodes_to_add {
4287 let new_node_id = remapper
4288 .get_node(&node.nodeid)
4289 .ok_or_else(|| {
4290 MutationError::InvalidSubgraph(format!("Node {} not mapped", node.nodeid))
4291 })?
4292 .clone();
4293
4294 let new_nodegroup_id = node.nodegroup_id.as_ref().and_then(|ng_id| {
4295 if *ng_id == root_nodegroup_id {
4296 target_nodegroup_id.clone()
4297 } else {
4298 remapper.get_nodegroup(ng_id).cloned()
4299 }
4300 });
4301
4302 let prefixed_name = if let Some(ref prefix) = params.name_prefix {
4303 format!("{} {}", prefix, node.name)
4304 } else {
4305 node.name.clone()
4306 };
4307
4308 let new_node = StaticNode {
4309 nodeid: new_node_id.clone(),
4310 name: prefixed_name,
4311 alias: remapper.get_alias(node.alias.as_deref()),
4312 datatype: node.datatype.clone(),
4313 nodegroup_id: new_nodegroup_id,
4314 graph_id: graph.graphid.clone(),
4315 is_collector: node.is_collector,
4316 isrequired: node.isrequired,
4317 exportable: node.exportable,
4318 sortorder: node.sortorder,
4319 config: node.config.clone(),
4320 parentproperty: node.parentproperty.clone(),
4321 ontologyclass: node.ontologyclass.clone(),
4322 description: node.description.clone(),
4323 fieldname: node.fieldname.clone(),
4324 hascustomalias: node.hascustomalias,
4325 issearchable: node.issearchable,
4326 istopnode: false,
4327 sourcebranchpublication_id: Some(branch_publication_id.clone()),
4328 source_identifier_id: node.source_identifier_id.clone(),
4329 is_immutable: node.is_immutable,
4330 };
4331 graph.push_node(new_node);
4332
4333 let original_edge = subgraph
4335 .edges
4336 .iter()
4337 .find(|e| e.domainnode_id == root_node_id && e.rangenode_id == node.nodeid);
4338 let new_edge_id = generate_uuid_v5(
4339 ("graph", Some(&graph.graphid)),
4340 &format!("update-subgraph-edge-{}-{}", target_node_id, new_node_id),
4341 );
4342 let new_edge = StaticEdge {
4343 edgeid: new_edge_id,
4344 domainnode_id: target_node_id.clone(),
4345 rangenode_id: new_node_id,
4346 graph_id: graph.graphid.clone(),
4347 name: original_edge.and_then(|e| e.name.clone()),
4348 ontologyproperty: if ontology_property.is_empty() {
4349 original_edge.and_then(|e| e.ontologyproperty.clone())
4350 } else {
4351 Some(ontology_property.clone())
4352 },
4353 description: original_edge.and_then(|e| e.description.clone()),
4354 source_identifier_id: None,
4355 };
4356 graph.push_edge(new_edge);
4357 }
4358
4359 for nodegroup in subgraph.nodegroups {
4361 if !new_node_nodegroups.contains(&nodegroup.nodegroupid) {
4362 continue;
4363 }
4364
4365 let new_ng_id = remapper
4366 .get_nodegroup(&nodegroup.nodegroupid)
4367 .ok_or_else(|| {
4368 MutationError::InvalidSubgraph(format!(
4369 "Nodegroup {} not mapped",
4370 nodegroup.nodegroupid
4371 ))
4372 })?
4373 .clone();
4374
4375 let new_parent_ng_id = nodegroup.parentnodegroup_id.as_ref().and_then(|parent_id| {
4376 if *parent_id == root_nodegroup_id {
4377 target_nodegroup_id.clone()
4378 } else {
4379 remapper.get_nodegroup(parent_id).cloned()
4380 }
4381 });
4382
4383 let new_grouping_node_id = nodegroup
4384 .grouping_node_id
4385 .as_ref()
4386 .and_then(|gn_id| remapper.get_node(gn_id).cloned());
4387
4388 let new_nodegroup = StaticNodegroup {
4389 nodegroupid: new_ng_id,
4390 cardinality: nodegroup.cardinality,
4391 parentnodegroup_id: new_parent_ng_id,
4392 legacygroupid: nodegroup.legacygroupid,
4393 grouping_node_id: new_grouping_node_id,
4394 };
4395 graph.push_nodegroup(new_nodegroup);
4396 }
4397 }
4398
4399 if remove_orphaned && !orphaned_node_ids.is_empty() {
4401 graph
4403 .nodes
4404 .retain(|n| !orphaned_node_ids.contains(&n.nodeid));
4405
4406 graph.edges.retain(|e| {
4408 !orphaned_node_ids.contains(&e.domainnode_id)
4409 && !orphaned_node_ids.contains(&e.rangenode_id)
4410 });
4411
4412 if let Some(ref mut cxnxws) = graph.cards_x_nodes_x_widgets {
4414 cxnxws.retain(|c| !orphaned_node_ids.contains(&c.node_id));
4415 }
4416 }
4417
4418 Ok(())
4419}
4420
4421fn find_branch_nodes_by_traversal(
4423 graph: &StaticGraph,
4424 target_node_id: &str,
4425 expected_branch_id: &str,
4426) -> Result<HashMap<String, Option<String>>, MutationError> {
4427 let mut branch_nodes: HashMap<String, Option<String>> = HashMap::new();
4428 let mut visited: HashSet<String> = HashSet::new();
4429 let mut queue: Vec<String> = Vec::new();
4430
4431 for edge in &graph.edges {
4433 if edge.domainnode_id == target_node_id {
4434 queue.push(edge.rangenode_id.clone());
4435 }
4436 }
4437
4438 while let Some(node_id) = queue.pop() {
4439 if visited.contains(&node_id) {
4440 continue;
4441 }
4442 visited.insert(node_id.clone());
4443
4444 let node = match graph.nodes.iter().find(|n| n.nodeid == node_id) {
4446 Some(n) => n,
4447 None => continue, };
4449
4450 match &node.sourcebranchpublication_id {
4452 Some(pub_id) if pub_id == expected_branch_id => {
4453 branch_nodes.insert(node_id.clone(), node.alias.clone());
4455
4456 for edge in &graph.edges {
4458 if edge.domainnode_id == node_id && !visited.contains(&edge.rangenode_id) {
4459 queue.push(edge.rangenode_id.clone());
4460 }
4461 }
4462 }
4463 Some(_pub_id) => {
4464 continue;
4468 }
4469 None => {
4470 continue;
4473 }
4474 }
4475 }
4476
4477 Ok(branch_nodes)
4478}
4479
4480#[derive(Debug, Clone, Serialize, Deserialize)]
4486pub struct MutationRequest {
4487 pub mutations: Vec<GraphMutation>,
4489 #[serde(default)]
4491 pub options: MutationRequestOptions,
4492}
4493
4494#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4496pub struct MutationRequestOptions {
4497 #[serde(default = "default_true")]
4499 pub autocreate_card: bool,
4500 #[serde(default = "default_true")]
4502 pub autocreate_widget: bool,
4503}
4504
4505fn default_true() -> bool {
4506 true
4507}
4508
4509impl From<MutationRequestOptions> for MutatorOptions {
4510 fn from(opts: MutationRequestOptions) -> Self {
4511 MutatorOptions {
4512 autocreate_card: opts.autocreate_card,
4513 autocreate_widget: opts.autocreate_widget,
4514 ontology_validator: None,
4515 skip_publication: false,
4516 }
4517 }
4518}
4519
4520pub fn apply_mutations_from_json(
4556 graph: &StaticGraph,
4557 mutations_json: &str,
4558) -> Result<StaticGraph, String> {
4559 apply_mutations_from_json_with_extensions(graph, mutations_json, None)
4560}
4561
4562pub fn apply_mutations_from_json_with_extensions(
4571 graph: &StaticGraph,
4572 mutations_json: &str,
4573 registry: Option<&ExtensionMutationRegistry>,
4574) -> Result<StaticGraph, String> {
4575 let request: MutationRequest = serde_json::from_str(mutations_json)
4577 .map_err(|e| format!("Failed to parse mutations JSON: {}", e))?;
4578
4579 apply_mutations_with_extensions(graph, request.mutations, request.options.into(), registry)
4580}
4581
4582pub fn apply_mutations_create_from_json(
4598 mutations_json: &str,
4599 graph: Option<&StaticGraph>,
4600) -> Result<StaticGraph, String> {
4601 let request: MutationRequest = serde_json::from_str(mutations_json)
4602 .map_err(|e| format!("Failed to parse mutations JSON: {}", e))?;
4603
4604 let mut mutations = request.mutations;
4605 let options: MutatorOptions = request.options.into();
4606
4607 match graph {
4608 None => {
4609 if mutations.is_empty() {
4611 return Err("No graph provided and no mutations to apply".to_string());
4612 }
4613
4614 let first = mutations.remove(0);
4615 match first {
4616 GraphMutation::CreateGraph(params) => {
4617 let mut new_graph = create_skeleton_graph(
4619 ¶ms.name,
4620 ¶ms.root_alias,
4621 params.is_resource,
4622 params.root_ontology_class.as_deref(),
4623 );
4624
4625 if let Some(ref custom_id) = params.graph_id {
4627 let old_id = new_graph.graphid.clone();
4629 new_graph.graphid = custom_id.clone();
4630 for node in &mut new_graph.nodes {
4631 if node.graph_id == old_id {
4632 node.graph_id = custom_id.clone();
4633 }
4634 }
4635 if new_graph.root.graph_id == old_id {
4636 new_graph.root.graph_id = custom_id.clone();
4637 }
4638 }
4639
4640 if let Some(author) = params.author {
4642 new_graph.author = Some(author);
4643 }
4644
4645 if let Some(desc) = params.description {
4647 new_graph.description = Some(StaticTranslatableString::from_string(&desc));
4648 }
4649
4650 if params.ontology_id.is_some() {
4652 new_graph.ontology_id = params.ontology_id;
4653 }
4654
4655 if mutations.is_empty() {
4657 if !options.skip_publication {
4658 stamp_publication(&mut new_graph);
4659 }
4660 new_graph.build_indices();
4661 Ok(new_graph)
4662 } else {
4663 apply_mutations_with_extensions(&new_graph, mutations, options, None)
4664 }
4665 }
4666 _ => Err("No graph provided and first mutation is not CreateGraph".to_string()),
4667 }
4668 }
4669 Some(existing_graph) => {
4670 if let Some(GraphMutation::CreateGraph(_)) = mutations.first() {
4672 return Err("CreateGraph cannot be used when a graph already exists".to_string());
4673 }
4674
4675 apply_mutations_with_extensions(existing_graph, mutations, options, None)
4676 }
4677 }
4678}
4679
4680pub fn apply_mutations(
4695 graph: &StaticGraph,
4696 mutations: Vec<GraphMutation>,
4697 options: MutatorOptions,
4698) -> Result<StaticGraph, String> {
4699 apply_mutations_with_extensions(graph, mutations, options, None)
4700}
4701
4702pub fn apply_mutations_with_extensions(
4741 graph: &StaticGraph,
4742 mutations: Vec<GraphMutation>,
4743 options: MutatorOptions,
4744 registry: Option<&ExtensionMutationRegistry>,
4745) -> Result<StaticGraph, String> {
4746 let mut result = graph.deep_clone();
4747
4748 for mutation in mutations {
4749 apply_mutation_with_extensions(&mut result, mutation, &options, registry)
4750 .map_err(|e| e.to_string())?;
4751 }
4752
4753 if !options.skip_publication {
4755 stamp_publication(&mut result);
4756 }
4757
4758 result.build_indices();
4759 Ok(result)
4760}
4761
4762fn stamp_publication(graph: &mut StaticGraph) {
4768 let now = chrono::Utc::now();
4769 let timestamp = now.timestamp_millis().to_string();
4770
4771 let publication_id = generate_uuid_v5(("publication", Some(&graph.graphid)), ×tamp);
4772 let published_time = now.format("%Y-%m-%dT%H:%M:%S%.3f").to_string();
4773
4774 graph.publication = Some(serde_json::json!({
4775 "publicationid": publication_id,
4776 "graph_id": graph.graphid,
4777 "published_time": published_time,
4778 "notes": null
4779 }));
4780}
4781
4782pub fn mutations_to_json(mutations: &[GraphMutation]) -> Result<String, String> {
4786 serde_json::to_string_pretty(mutations)
4787 .map_err(|e| format!("Failed to serialize mutations: {}", e))
4788}
4789
4790pub fn create_skeleton_graph(
4819 name: &str,
4820 root_alias: &str,
4821 is_resource: bool,
4822 ontology_classes: Option<&[String]>,
4823) -> StaticGraph {
4824 let graphid = generate_uuid_v5(("skeleton", None), name);
4826
4827 let root_nodeid = generate_uuid_v5(("graph", Some(&graphid)), &format!("root-{}", root_alias));
4829
4830 let root_nodegroup_id: serde_json::Value = if is_resource {
4834 serde_json::Value::Null
4835 } else {
4836 serde_json::Value::String(root_nodeid.clone())
4837 };
4838 let root_is_collector = !is_resource;
4839
4840 let ontology_class_json = match ontology_classes {
4843 None | Some([]) => serde_json::Value::Null,
4844 Some([single]) => serde_json::Value::String(single.clone()),
4845 Some(list) => serde_json::Value::Array(
4846 list.iter()
4847 .map(|s| serde_json::Value::String(s.clone()))
4848 .collect(),
4849 ),
4850 };
4851
4852 let graph_json = serde_json::json!({
4854 "graphid": graphid,
4855 "name": { "en": name },
4856 "isresource": is_resource,
4857 "is_active": is_resource,
4858 "is_editable": true,
4859 "config": {},
4860 "template_id": "50000000-0000-0000-0000-000000000001",
4861 "version": "1",
4862 "nodes": [{
4863 "nodeid": root_nodeid,
4864 "name": name,
4865 "alias": root_alias,
4866 "datatype": "semantic",
4867 "nodegroup_id": root_nodegroup_id,
4868 "graph_id": graphid,
4869 "is_collector": root_is_collector,
4870 "isrequired": false,
4871 "exportable": true,
4872 "sortorder": 0,
4873 "istopnode": true,
4874 "issearchable": true,
4875 "ontologyclass": ontology_class_json.clone()
4876 }],
4877 "root": {
4878 "nodeid": root_nodeid,
4879 "name": name,
4880 "alias": root_alias,
4881 "datatype": "semantic",
4882 "nodegroup_id": root_nodegroup_id,
4883 "graph_id": graphid,
4884 "is_collector": root_is_collector,
4885 "isrequired": false,
4886 "exportable": true,
4887 "sortorder": 0,
4888 "istopnode": true,
4889 "issearchable": true,
4890 "ontologyclass": ontology_class_json.clone()
4891 },
4892 "nodegroups": [],
4893 "edges": [],
4894 "cards": [],
4895 "cards_x_nodes_x_widgets": [],
4896 "functions_x_graphs": []
4897 });
4898
4899 let mut graph: StaticGraph =
4900 serde_json::from_value(graph_json).expect("Failed to create skeleton graph");
4901 graph.build_indices();
4902 graph
4903}
4904
4905#[derive(Debug, Clone, Serialize, Deserialize)]
4935pub struct GraphInstruction {
4936 pub action: String,
4938 pub subject: String,
4940 #[serde(default)]
4942 pub object: String,
4943 #[serde(default)]
4945 pub params: HashMap<String, serde_json::Value>,
4946}
4947
4948impl GraphInstruction {
4949 pub fn new(action: &str, subject: &str, object: &str) -> Self {
4951 Self {
4952 action: action.to_string(),
4953 subject: subject.to_string(),
4954 object: object.to_string(),
4955 params: HashMap::new(),
4956 }
4957 }
4958
4959 pub fn with_param(mut self, key: &str, value: serde_json::Value) -> Self {
4961 self.params.insert(key.to_string(), value);
4962 self
4963 }
4964
4965 pub fn with_str(self, key: &str, value: &str) -> Self {
4967 self.with_param(key, serde_json::Value::String(value.to_string()))
4968 }
4969
4970 fn get_str(&self, key: &str) -> Option<String> {
4972 self.params
4973 .get(key)
4974 .and_then(|v| v.as_str())
4975 .map(|s| s.to_string())
4976 }
4977
4978 fn get_str_or(&self, key: &str, default: &str) -> String {
4980 self.get_str(key).unwrap_or_else(|| default.to_string())
4981 }
4982
4983 fn get_class_list(&self, key: &str) -> Option<Vec<String>> {
4987 let raw: Vec<String> = match self.params.get(key)? {
4988 serde_json::Value::String(s) => vec![s.clone()],
4989 serde_json::Value::Array(arr) => arr
4990 .iter()
4991 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4992 .collect(),
4993 _ => return None,
4994 };
4995 sanitize_class_list(Some(raw))
4996 }
4997
4998 fn resolve_subgraph(&self) -> Result<StaticGraph, MutationError> {
5001 if let Some(subgraph_value) = self.params.get("subgraph") {
5002 serde_json::from_value(subgraph_value.clone()).map_err(|e| {
5003 MutationError::InvalidSubgraph(format!("Failed to parse subgraph: {}", e))
5004 })
5005 } else if !self.object.is_empty() {
5006 let graph = crate::registry::get_graph(&self.object).ok_or_else(|| {
5007 MutationError::InvalidSubgraph(format!(
5008 "Branch '{}' not found in graph registry",
5009 self.object
5010 ))
5011 })?;
5012 Ok((*graph).clone())
5013 } else {
5014 Err(MutationError::InvalidSubgraph(
5015 "add_subgraph/update_subgraph requires either 'subgraph' param or a branch graph ID as object".to_string(),
5016 ))
5017 }
5018 }
5019
5020 fn get_translatable_map(&self, key: &str) -> Option<HashMap<String, String>> {
5022 self.params.get(key).and_then(|v| {
5023 if let Some(obj) = v.as_object() {
5024 let map: HashMap<String, String> = obj
5025 .iter()
5026 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
5027 .collect();
5028 if map.is_empty() {
5029 None
5030 } else {
5031 Some(map)
5032 }
5033 } else {
5034 None
5035 }
5036 })
5037 }
5038
5039 pub fn to_mutation(&self) -> Result<GraphMutation, MutationError> {
5041 match self.action.as_str() {
5042 "add_node" => {
5043 let cardinality_str = self.get_str_or("cardinality", "1");
5044 let cardinality = match cardinality_str.as_str() {
5045 "1" | "one" | "One" => Cardinality::One,
5046 "n" | "N" | "many" => Cardinality::N,
5047 _ => {
5048 return Err(MutationError::InvalidSubgraph(format!(
5049 "Invalid cardinality: {}",
5050 cardinality_str
5051 )))
5052 }
5053 };
5054
5055 Ok(GraphMutation::AddNode(AddNodeParams {
5056 parent_alias: if self.subject.is_empty() {
5057 None
5058 } else {
5059 Some(self.subject.clone())
5060 },
5061 alias: self.object.clone(),
5062 name: self.get_str_or("name", &self.object),
5063 cardinality,
5064 datatype: self.get_str_or("datatype", "semantic"),
5065 ontology_class: self.get_class_list("ontology_class"),
5066 parent_property: self.get_str_or("parent_property", ""),
5067 description: self.get_str("description"),
5068 config: self.params.get("config").cloned(),
5069 options: {
5070 let mut opts: NodeOptions = self
5071 .params
5072 .get("options")
5073 .and_then(|v| serde_json::from_value(v.clone()).ok())
5074 .unwrap_or_default();
5075 if opts.is_collector.is_none() {
5077 let dt = self.get_str_or("datatype", "semantic");
5078 if dt == "semantic" && cardinality == Cardinality::N {
5079 opts.is_collector = Some(true);
5080 }
5081 }
5082 opts
5083 },
5084 }))
5085 }
5086 "add_edge" => Ok(GraphMutation::AddEdge(AddEdgeParams {
5087 from_node_id: self.subject.clone(),
5088 to_node_id: self.object.clone(),
5089 ontology_property: self.get_str_or("ontology_property", ""),
5090 name: self.get_str("name"),
5091 description: self.get_str("description"),
5092 })),
5093 "add_nodegroup" => {
5094 let cardinality_str = self.get_str_or("cardinality", "n");
5095 let cardinality = match cardinality_str.as_str() {
5096 "1" | "one" | "One" => Cardinality::One,
5097 "n" | "N" | "many" => Cardinality::N,
5098 _ => Cardinality::N,
5099 };
5100
5101 let nodegroup_id = self
5103 .get_str("nodegroup_id")
5104 .unwrap_or_else(|| format!("ng-{}", self.subject));
5105
5106 Ok(GraphMutation::AddNodegroup(AddNodegroupParams {
5107 nodegroup_id,
5108 cardinality,
5109 parent_alias: if self.subject.is_empty() {
5110 None
5111 } else {
5112 Some(self.subject.clone())
5113 },
5114 }))
5115 }
5116 "add_card" => {
5117 let name = if self.object.is_empty() {
5118 StaticTranslatableString::from_string("Card")
5119 } else {
5120 StaticTranslatableString::from_string(&self.object)
5121 };
5122 Ok(GraphMutation::AddCard(AddCardParams {
5123 nodegroup_id: self.subject.clone(),
5124 name,
5125 component_id: self.get_str("component_id"),
5126 options: CardOptions {
5127 description: self
5128 .get_str("description")
5129 .map(|s| StaticTranslatableString::from_string(&s)),
5130 ..CardOptions::default()
5131 },
5132 config: self.params.get("config").cloned(),
5133 }))
5134 }
5135 "add_widget" => Ok(GraphMutation::AddWidgetToCard(AddWidgetParams {
5136 node_id: self.subject.clone(),
5137 widget_id: self.get_str_or("widget_id", "10000000-0000-0000-0000-000000000001"),
5138 label: self.get_str_or("label", ""),
5139 config: self
5140 .params
5141 .get("config")
5142 .cloned()
5143 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
5144 sortorder: self
5145 .params
5146 .get("sortorder")
5147 .and_then(|v| v.as_i64())
5148 .map(|i| i as i32),
5149 visible: self.params.get("visible").and_then(|v| v.as_bool()),
5150 })),
5151 "add_subgraph" => {
5152 let subgraph = self.resolve_subgraph()?;
5153
5154 Ok(GraphMutation::AddSubgraph(AddSubgraphParams {
5155 subgraph,
5156 target_node_id: self.subject.clone(),
5157 ontology_property: self.get_str_or("ontology_property", ""),
5158 alias_suffix: self.get_str("alias_suffix"),
5159 alias_prefix: self.get_str("alias_prefix"),
5160 name_prefix: self.get_str("name_prefix"),
5161 }))
5162 }
5163 "update_subgraph" => {
5164 let subgraph = self.resolve_subgraph()?;
5165
5166 let remove_orphaned = self
5167 .params
5168 .get("remove_orphaned")
5169 .and_then(|v| v.as_bool())
5170 .unwrap_or(false);
5171
5172 Ok(GraphMutation::UpdateSubgraph(UpdateSubgraphParams {
5173 subgraph,
5174 target_node_id: self.subject.clone(),
5175 ontology_property: self.get_str_or("ontology_property", ""),
5176 alias_suffix: self.get_str("alias_suffix"),
5177 remove_orphaned,
5178 alias_prefix: self.get_str("alias_prefix"),
5179 name_prefix: self.get_str("name_prefix"),
5180 }))
5181 }
5182 "concept_change_collection" => Ok(GraphMutation::ConceptChangeCollection(
5183 ConceptChangeCollectionParams {
5184 node_id: self.subject.clone(),
5185 collection_id: self.object.clone(),
5186 },
5187 )),
5188 "delete_card" => Ok(GraphMutation::DeleteCard(DeleteCardParams {
5189 card_id: self.subject.clone(),
5190 })),
5191 "delete_widget" => Ok(GraphMutation::DeleteWidget(DeleteWidgetParams {
5192 widget_mapping_id: self.subject.clone(),
5193 })),
5194 "add_function" => Ok(GraphMutation::AddFunction(AddFunctionParams {
5195 function_id: self.subject.clone(),
5196 config: self.params.get("config").cloned(),
5197 })),
5198 "set_descriptor_function" => Ok(GraphMutation::SetDescriptorFunction(
5199 SetDescriptorFunctionParams {
5200 function_id: self.subject.clone(),
5201 },
5202 )),
5203 "delete_function" => Ok(GraphMutation::DeleteFunction(DeleteFunctionParams {
5204 function_mapping_id: self.subject.clone(),
5205 })),
5206 "set_descriptor_template" => Ok(GraphMutation::SetDescriptorTemplate(
5207 SetDescriptorTemplateParams {
5208 descriptor_type: self.subject.clone(),
5209 string_template: self.object.clone(),
5210 },
5211 )),
5212 "delete_node" => Ok(GraphMutation::DeleteNode(DeleteNodeParams {
5213 node_id: self.subject.clone(),
5214 })),
5215 "delete_nodegroup" => Ok(GraphMutation::DeleteNodegroup(DeleteNodegroupParams {
5216 nodegroup_id: self.subject.clone(),
5217 })),
5218 "update_node" => Ok(GraphMutation::UpdateNode(UpdateNodeParams {
5219 node_id: self.subject.clone(),
5220 name: self.get_str("name"),
5221 ontology_class: self.get_class_list("ontology_class"),
5222 parent_property: self.get_str("parent_property"),
5223 description: self.get_str("description"),
5224 config: self.params.get("config").cloned(),
5225 options: UpdateNodeOptions {
5226 exportable: self.params.get("exportable").and_then(|v| v.as_bool()),
5227 fieldname: self.get_str("fieldname"),
5228 isrequired: self.params.get("isrequired").and_then(|v| v.as_bool()),
5229 issearchable: self.params.get("issearchable").and_then(|v| v.as_bool()),
5230 sortorder: self
5231 .params
5232 .get("sortorder")
5233 .and_then(|v| v.as_i64())
5234 .map(|i| i as i32),
5235 },
5236 })),
5237 "change_node_type" => {
5238 let datatype = self
5239 .get_str("datatype")
5240 .or_else(|| {
5241 if self.object.is_empty() {
5242 None
5243 } else {
5244 Some(self.object.clone())
5245 }
5246 })
5247 .ok_or_else(|| {
5248 MutationError::InvalidSubgraph(
5249 "change_node_type requires 'datatype' param or object".to_string(),
5250 )
5251 })?;
5252
5253 Ok(GraphMutation::ChangeNodeType(ChangeNodeTypeParams {
5254 node_id: self.subject.clone(),
5255 datatype,
5256 name: self.get_str("name"),
5257 ontology_class: self.get_class_list("ontology_class"),
5258 parent_property: self.get_str("parent_property"),
5259 description: self.get_str("description"),
5260 config: self.params.get("config").cloned(),
5261 options: UpdateNodeOptions {
5262 exportable: self.params.get("exportable").and_then(|v| v.as_bool()),
5263 fieldname: self.get_str("fieldname"),
5264 isrequired: self.params.get("isrequired").and_then(|v| v.as_bool()),
5265 issearchable: self.params.get("issearchable").and_then(|v| v.as_bool()),
5266 sortorder: self
5267 .params
5268 .get("sortorder")
5269 .and_then(|v| v.as_i64())
5270 .map(|i| i as i32),
5271 },
5272 }))
5273 }
5274 "change_cardinality" => {
5275 let cardinality_str = self
5276 .get_str("cardinality")
5277 .or_else(|| {
5278 if self.object.is_empty() {
5279 None
5280 } else {
5281 Some(self.object.clone())
5282 }
5283 })
5284 .ok_or_else(|| {
5285 MutationError::InvalidSubgraph(
5286 "change_cardinality requires 'cardinality' param or object (1 or n)"
5287 .to_string(),
5288 )
5289 })?;
5290
5291 let cardinality = match cardinality_str.to_lowercase().as_str() {
5292 "1" | "one" => Cardinality::One,
5293 "n" | "many" => Cardinality::N,
5294 _ => {
5295 return Err(MutationError::InvalidSubgraph(format!(
5296 "Invalid cardinality '{}', expected '1', 'one', 'n', or 'many'",
5297 cardinality_str
5298 )))
5299 }
5300 };
5301
5302 Ok(GraphMutation::ChangeCardinality(ChangeCardinalityParams {
5303 node_id: self.subject.clone(),
5304 cardinality,
5305 }))
5306 }
5307 "rename_node" => Ok(GraphMutation::RenameNode(RenameNodeParams {
5308 node_id: self.subject.clone(),
5309 alias: self.get_str("alias").or_else(|| {
5310 if self.object.is_empty() {
5311 None
5312 } else {
5313 Some(self.object.clone())
5314 }
5315 }),
5316 name: self.get_str("name"),
5317 description: self.get_str("description"),
5318 realign_card: self
5319 .params
5320 .get("realign_card")
5321 .and_then(|v| v.as_bool())
5322 .unwrap_or(true),
5323 })),
5324 "rename_card" => {
5325 let name = self.get_str("name").or_else(|| {
5328 if self.object.is_empty() {
5329 None
5330 } else {
5331 Some(self.object.clone())
5332 }
5333 });
5334 Ok(GraphMutation::RenameCard(RenameCardParams {
5335 card_id: self.subject.clone(),
5336 language: self.get_str("language"),
5337 name,
5338 name_i18n: self.get_translatable_map("name_i18n"),
5339 description: self.get_str("description"),
5340 description_i18n: self.get_translatable_map("description_i18n"),
5341 }))
5342 }
5343 "realign_card_from_node" => Ok(GraphMutation::RealignCardFromNode(
5344 RealignCardFromNodeParams {
5345 node_alias: self.subject.clone(),
5346 },
5347 )),
5348 "rename_graph" => {
5349 let name = self.get_translatable_map("name").or_else(|| {
5351 if self.object.is_empty() {
5352 None
5353 } else {
5354 let mut map = HashMap::new();
5355 map.insert("en".to_string(), self.object.clone());
5356 Some(map)
5357 }
5358 });
5359 Ok(GraphMutation::RenameGraph(RenameGraphParams {
5360 name,
5361 description: self.get_translatable_map("description"),
5362 subtitle: self.get_translatable_map("subtitle"),
5363 author: self.get_str("author"),
5364 }))
5365 }
5366 "update_widget_config" => {
5367 let config = self.params.get("config").cloned().ok_or_else(|| {
5368 MutationError::Other("update_widget_config requires params.config".to_string())
5369 })?;
5370 Ok(GraphMutation::UpdateWidgetConfig(
5371 UpdateWidgetConfigParams {
5372 node_id: self.subject.clone(),
5373 config,
5374 },
5375 ))
5376 }
5377 "coppice_subgraph" => {
5378 let publication_id = self.get_str("publication_id").ok_or_else(|| {
5379 MutationError::Other(
5380 "coppice_subgraph requires params.publication_id".to_string(),
5381 )
5382 })?;
5383 Ok(GraphMutation::CoppiceSubgraph(CoppiceSubgraphParams {
5384 subject: self.subject.clone(),
5385 publication_id,
5386 }))
5387 }
5388 "create_model" | "create_branch" => Err(MutationError::InvalidSubgraph(format!(
5390 "'{}' creates a new graph, use build_graph_from_instructions() instead",
5391 self.action
5392 ))),
5393 other => Ok(GraphMutation::Extension(ExtensionMutationParams {
5395 name: other.to_string(),
5396 params: {
5397 let mut map = serde_json::Map::new();
5398 if !self.subject.is_empty() {
5400 map.insert(
5401 "subject".to_string(),
5402 serde_json::Value::String(self.subject.clone()),
5403 );
5404 }
5405 if !self.object.is_empty() {
5406 map.insert(
5407 "object".to_string(),
5408 serde_json::Value::String(self.object.clone()),
5409 );
5410 }
5411 for (k, v) in &self.params {
5413 map.insert(k.clone(), v.clone());
5414 }
5415 serde_json::Value::Object(map)
5416 },
5417 conformance: MutationConformance::AlwaysConformant,
5418 })),
5419 }
5420 }
5421
5422 pub fn is_create_action(&self) -> bool {
5424 matches!(
5425 self.action.as_str(),
5426 "create_model" | "create_branch" | "load_graph"
5427 )
5428 }
5429
5430 pub fn conformance(&self) -> MutationConformance {
5432 match self.action.as_str() {
5433 "add_node" | "add_edge" | "add_nodegroup" | "add_card" | "add_widget" => {
5435 MutationConformance::BranchConformant
5436 }
5437 "add_subgraph" | "update_subgraph" | "add_function" | "set_descriptor_function" => {
5439 MutationConformance::ModelConformant
5440 }
5441 "concept_change_collection" => MutationConformance::AlwaysConformant,
5443 "delete_card" | "delete_widget" | "delete_function" | "delete_node"
5445 | "delete_nodegroup" => MutationConformance::AlwaysConformant,
5446 "update_node" | "change_node_type" | "change_cardinality" => {
5448 MutationConformance::BranchConformant
5449 }
5450 "rename_node" | "rename_graph" | "coppice_subgraph" => {
5451 MutationConformance::AlwaysConformant
5452 }
5453 "create_model" => MutationConformance::ModelConformant,
5455 "create_branch" => MutationConformance::BranchConformant,
5456 _ => MutationConformance::NonConformant,
5458 }
5459 }
5460
5461 pub fn to_skeleton_graph(&self) -> Result<StaticGraph, MutationError> {
5472 let is_resource = match self.action.as_str() {
5473 "create_model" => true,
5474 "create_branch" => false,
5475 _ => {
5476 return Err(MutationError::InvalidSubgraph(format!(
5477 "'{}' is not a create action, use to_mutation() instead",
5478 self.action
5479 )))
5480 }
5481 };
5482
5483 let root_alias = &self.subject;
5484 let name = self.get_str_or("name", root_alias);
5485 let ontology_classes = self.get_class_list("ontology_class");
5486
5487 let mut graph =
5489 create_skeleton_graph(&name, root_alias, is_resource, ontology_classes.as_deref());
5490
5491 let ontology_ids = self.get_class_list("ontology_id");
5493 if ontology_ids.is_some() {
5494 graph.ontology_id = ontology_ids;
5495 }
5496
5497 if !self.object.is_empty() {
5499 let new_graphid = self.object.clone();
5500 graph.graphid = new_graphid.clone();
5502 graph.root.graph_id = new_graphid.clone();
5503 for node in &mut graph.nodes {
5504 node.graph_id = new_graphid.clone();
5505 }
5506 }
5507
5508 graph.slug = self
5510 .get_str("slug")
5511 .or_else(|| Some(root_alias.to_lowercase()));
5512
5513 Ok(graph)
5514 }
5515}
5516
5517pub fn build_graph_from_instructions(
5542 instructions: Vec<GraphInstruction>,
5543 options: MutatorOptions,
5544) -> Result<StaticGraph, String> {
5545 build_graph_from_instructions_with_extensions(instructions, options, None)
5546}
5547
5548pub fn build_graph_from_instructions_with_extensions(
5553 instructions: Vec<GraphInstruction>,
5554 options: MutatorOptions,
5555 registry: Option<&ExtensionMutationRegistry>,
5556) -> Result<StaticGraph, String> {
5557 if instructions.is_empty() {
5558 return Err("No instructions provided".to_string());
5559 }
5560
5561 let mut iter = instructions.into_iter();
5562 let first = iter.next().unwrap();
5563
5564 if !first.is_create_action() {
5566 return Err(format!(
5567 "First instruction must be 'create_model', 'create_branch', or 'load_graph', got '{}'",
5568 first.action
5569 ));
5570 }
5571
5572 let graph = if first.action == "load_graph" {
5573 let graph_id = &first.subject;
5574 let arc = crate::registry::get_graph(graph_id).ok_or_else(|| {
5575 format!(
5576 "Graph '{}' not found in registry. Call register_graph() first.",
5577 graph_id
5578 )
5579 })?;
5580 (*arc).clone()
5581 } else {
5582 first.to_skeleton_graph().map_err(|e| e.to_string())?
5583 };
5584
5585 let remaining: Vec<GraphInstruction> = iter.collect();
5587 if remaining.is_empty() {
5588 let mut graph = graph;
5589 if !options.skip_publication {
5590 stamp_publication(&mut graph);
5591 }
5592 return Ok(graph);
5593 }
5594
5595 apply_instructions(&graph, remaining, options, registry)
5596}
5597
5598pub fn build_graph_from_instructions_json(json: &str) -> Result<StaticGraph, String> {
5611 #[derive(Deserialize)]
5612 struct BuildRequest {
5613 instructions: Vec<GraphInstruction>,
5614 #[serde(default)]
5615 options: MutationRequestOptions,
5616 }
5617
5618 let request: BuildRequest = serde_json::from_str(json)
5619 .map_err(|e| format!("Failed to parse build request JSON: {}", e))?;
5620
5621 build_graph_from_instructions(request.instructions, request.options.into())
5622}
5623
5624pub fn parse_instructions_from_csv(csv_text: &str) -> Result<Vec<GraphInstruction>, String> {
5636 let filtered: String = csv_text
5639 .lines()
5640 .filter(|line| {
5641 let trimmed = line.trim();
5642 !trimmed.starts_with('#')
5643 })
5644 .collect::<Vec<_>>()
5645 .join("\n");
5646
5647 let mut reader = csv::Reader::from_reader(filtered.as_bytes());
5648 let headers = reader
5649 .headers()
5650 .map_err(|e| format!("Failed to parse CSV headers: {}", e))?
5651 .clone();
5652
5653 let param_indices: Vec<(usize, String)> = headers
5654 .iter()
5655 .enumerate()
5656 .filter_map(|(i, h)| h.strip_prefix("params.").map(|p| (i, p.to_string())))
5657 .collect();
5658
5659 let action_idx = headers
5660 .iter()
5661 .position(|h| h == "action")
5662 .ok_or("CSV missing 'action' column")?;
5663 let subject_idx = headers
5664 .iter()
5665 .position(|h| h == "subject")
5666 .ok_or("CSV missing 'subject' column")?;
5667 let object_idx = headers
5668 .iter()
5669 .position(|h| h == "object")
5670 .ok_or("CSV missing 'object' column")?;
5671
5672 let mut instructions = Vec::new();
5673 for result in reader.records() {
5674 let record = result.map_err(|e| format!("Failed to parse CSV row: {}", e))?;
5675
5676 let action = record.get(action_idx).unwrap_or("").to_string();
5677 if action.is_empty() || action.starts_with('#') {
5678 continue;
5679 }
5680
5681 let mut params = serde_json::Map::new();
5682 for (idx, param_name) in ¶m_indices {
5683 if let Some(value) = record.get(*idx) {
5684 if !value.is_empty() {
5685 let json_value = serde_json::from_str(value)
5687 .unwrap_or(serde_json::Value::String(value.to_string()));
5688 let parts: Vec<&str> = param_name.splitn(2, '.').collect();
5690 if parts.len() == 2 {
5691 let outer = parts[0];
5692 let inner = parts[1];
5693 let nested = params
5694 .entry(outer.to_string())
5695 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
5696 if let serde_json::Value::Object(ref mut map) = nested {
5697 map.insert(inner.to_string(), json_value);
5698 }
5699 } else {
5700 params.insert(param_name.clone(), json_value);
5701 }
5702 }
5703 }
5704 }
5705
5706 instructions.push(GraphInstruction {
5707 action,
5708 subject: record.get(subject_idx).unwrap_or("").to_string(),
5709 object: record.get(object_idx).unwrap_or("").to_string(),
5710 params: serde_json::Value::Object(params)
5711 .as_object()
5712 .unwrap()
5713 .iter()
5714 .map(|(k, v)| (k.clone(), v.clone()))
5715 .collect(),
5716 });
5717 }
5718
5719 Ok(instructions)
5720}
5721
5722pub fn build_graph_from_instructions_csv(
5734 csv_text: &str,
5735 options: MutatorOptions,
5736) -> Result<StaticGraph, String> {
5737 let instructions = parse_instructions_from_csv(csv_text)?;
5738 build_graph_from_instructions(instructions, options)
5739}
5740
5741pub fn apply_instructions(
5754 graph: &StaticGraph,
5755 instructions: Vec<GraphInstruction>,
5756 options: MutatorOptions,
5757 registry: Option<&ExtensionMutationRegistry>,
5758) -> Result<StaticGraph, String> {
5759 let mutations: Vec<GraphMutation> = instructions
5760 .into_iter()
5761 .map(|i| i.to_mutation())
5762 .collect::<Result<Vec<_>, _>>()
5763 .map_err(|e| e.to_string())?;
5764
5765 apply_mutations_with_extensions(graph, mutations, options, registry)
5766}
5767
5768pub fn apply_instructions_from_json(
5780 graph: &StaticGraph,
5781 json: &str,
5782) -> Result<StaticGraph, String> {
5783 #[derive(Deserialize)]
5784 struct InstructionRequest {
5785 instructions: Vec<GraphInstruction>,
5786 #[serde(default)]
5787 options: MutationRequestOptions,
5788 }
5789
5790 let request: InstructionRequest = serde_json::from_str(json)
5791 .map_err(|e| format!("Failed to parse instructions JSON: {}", e))?;
5792
5793 apply_instructions(graph, request.instructions, request.options.into(), None)
5794}
5795
5796pub fn get_mutation_schema() -> serde_json::Value {
5800 serde_json::json!({
5801 "MutationRequest": {
5802 "description": "Container for a list of mutations to apply",
5803 "properties": {
5804 "mutations": {
5805 "type": "array",
5806 "items": { "$ref": "#/GraphMutation" }
5807 },
5808 "options": { "$ref": "#/MutationRequestOptions" }
5809 }
5810 },
5811 "MutationRequestOptions": {
5812 "properties": {
5813 "autocreate_card": { "type": "boolean", "default": true },
5814 "autocreate_widget": { "type": "boolean", "default": true }
5815 }
5816 },
5817 "GraphMutation": {
5818 "oneOf": [
5819 { "AddNode": { "$ref": "#/AddNodeParams" } },
5820 { "AddNodegroup": { "$ref": "#/AddNodegroupParams" } },
5821 { "AddEdge": { "$ref": "#/AddEdgeParams" } },
5822 { "AddCard": { "$ref": "#/AddCardParams" } },
5823 { "AddWidgetToCard": { "$ref": "#/AddWidgetParams" } },
5824 { "CreateGraph": { "$ref": "#/CreateGraphParams" } },
5825 { "SetDescriptorTemplate": { "$ref": "#/SetDescriptorTemplateParams" } }
5826 ]
5827 },
5828 "SetDescriptorTemplateParams": {
5829 "required": ["descriptor_type", "string_template"],
5830 "properties": {
5831 "descriptor_type": { "type": "string", "description": "Descriptor type (e.g. 'name', 'slug', 'description', 'map_popup')" },
5832 "string_template": { "type": "string", "description": "String template with <Node Name> placeholders" }
5833 }
5834 },
5835 "CreateGraphParams": {
5836 "required": ["name", "is_resource", "root_alias"],
5837 "properties": {
5838 "name": { "type": "string", "description": "Name for the graph" },
5839 "is_resource": { "type": "boolean", "description": "Whether this is a resource model (true) or branch (false)" },
5840 "root_alias": { "type": "string", "description": "Alias for the root node" },
5841 "root_ontology_class": {
5842 "oneOf": [
5843 { "type": "string" },
5844 { "type": "array", "items": { "type": "string" } },
5845 { "type": "null" }
5846 ],
5847 "nullable": true,
5848 "description": "Ontology class URI(s) for the root node. Accepts a single string or an array of strings."
5849 },
5850 "graph_id": { "type": "string", "nullable": true, "description": "Optional custom graph ID" },
5851 "author": { "type": "string", "nullable": true, "description": "Optional author" },
5852 "description": { "type": "string", "nullable": true, "description": "Optional description" }
5853 }
5854 },
5855 "AddNodeParams": {
5856 "required": ["alias", "name", "cardinality", "datatype", "parent_property"],
5857 "properties": {
5858 "parent_alias": { "type": "string", "nullable": true },
5859 "alias": { "type": "string" },
5860 "name": { "type": "string" },
5861 "cardinality": { "enum": ["One", "N"] },
5862 "datatype": { "type": "string", "examples": ["semantic", "string", "number", "date", "boolean", "concept", "concept-list"] },
5863 "ontology_class": {
5864 "oneOf": [
5865 { "type": "string" },
5866 { "type": "array", "items": { "type": "string" } },
5867 { "type": "null" }
5868 ],
5869 "nullable": true,
5870 "description": "Ontology class URI(s) for the node. Accepts a single string or an array of strings."
5871 },
5872 "parent_property": { "type": "string" },
5873 "description": { "type": "string", "nullable": true },
5874 "config": { "type": "object", "nullable": true },
5875 "options": { "$ref": "#/NodeOptions" }
5876 }
5877 },
5878 "NodeOptions": {
5879 "properties": {
5880 "exportable": { "type": "boolean" },
5881 "fieldname": { "type": "string" },
5882 "hascustomalias": { "type": "boolean" },
5883 "is_collector": { "type": "boolean" },
5884 "isrequired": { "type": "boolean" },
5885 "issearchable": { "type": "boolean" },
5886 "istopnode": { "type": "boolean" },
5887 "sortorder": { "type": "integer" }
5888 }
5889 },
5890 "Cardinality": {
5891 "enum": ["One", "N"],
5892 "description": "One = single instance only, N = multiple instances allowed"
5893 }
5894 })
5895}
5896
5897#[cfg(test)]
5898mod tests {
5899 use super::*;
5900
5901 fn create_test_graph() -> StaticGraph {
5902 let graph_json = r#"{
5903 "graphid": "test-graph-id",
5904 "name": {"en": "Test Graph"},
5905 "isresource": true,
5906 "is_editable": true,
5907 "nodes": [{
5908 "nodeid": "root-node-id",
5909 "name": "Root",
5910 "alias": "root",
5911 "datatype": "semantic",
5912 "nodegroup_id": "root-nodegroup",
5913 "graph_id": "test-graph-id",
5914 "is_collector": false,
5915 "isrequired": false,
5916 "exportable": false,
5917 "ontologyclass": "E1_CRM_Entity",
5918 "hascustomalias": false,
5919 "issearchable": false,
5920 "istopnode": true
5921 }],
5922 "nodegroups": [{
5923 "nodegroupid": "root-nodegroup",
5924 "cardinality": "1"
5925 }],
5926 "edges": [],
5927 "cards": [],
5928 "cards_x_nodes_x_widgets": [],
5929 "root": {
5930 "nodeid": "root-node-id",
5931 "name": "Root",
5932 "alias": "root",
5933 "datatype": "semantic",
5934 "nodegroup_id": "root-nodegroup",
5935 "graph_id": "test-graph-id",
5936 "is_collector": false,
5937 "isrequired": false,
5938 "exportable": false,
5939 "ontologyclass": "E1_CRM_Entity",
5940 "hascustomalias": false,
5941 "issearchable": false,
5942 "istopnode": true
5943 }
5944 }"#;
5945
5946 let mut graph: StaticGraph =
5947 serde_json::from_str(graph_json).expect("Failed to parse test graph JSON");
5948 graph.build_indices();
5949 graph
5950 }
5951
5952 #[test]
5953 fn test_uuid_generation() {
5954 let uuid1 = generate_uuid_v5(("graph", Some("test-id")), "node-1");
5955 let uuid2 = generate_uuid_v5(("graph", Some("test-id")), "node-1");
5956 let uuid3 = generate_uuid_v5(("graph", Some("test-id")), "node-2");
5957
5958 assert_eq!(uuid1, uuid2);
5960 assert_ne!(uuid1, uuid3);
5962 assert!(Uuid::parse_str(&uuid1).is_ok());
5964 }
5965
5966 #[test]
5967 fn test_add_semantic_node() {
5968 let graph = create_test_graph();
5969
5970 let result = GraphMutator::new(graph)
5971 .add_semantic_node(
5972 Some("root"),
5973 "child",
5974 "Child Node",
5975 Cardinality::N,
5976 "E1_CRM_Entity",
5977 "P1_is_identified_by",
5978 Some("A child node"),
5979 NodeOptions::default(),
5980 None,
5981 )
5982 .build();
5983
5984 assert!(result.is_ok());
5985 let built_graph = result.unwrap();
5986
5987 assert_eq!(built_graph.nodes.len(), 2);
5989
5990 assert_eq!(built_graph.nodegroups.len(), 2);
5992
5993 assert_eq!(built_graph.edges.len(), 1);
5995
5996 assert_eq!(built_graph.cards_slice().len(), 1);
5998 }
5999
6000 #[test]
6001 fn test_add_string_node() {
6002 let graph = create_test_graph();
6003
6004 let result = GraphMutator::new(graph)
6005 .add_string_node(
6006 Some("root"),
6007 "name",
6008 "Name",
6009 Cardinality::One,
6010 "E41_Appellation",
6011 "P1_is_identified_by",
6012 None,
6013 NodeOptions::default(),
6014 None,
6015 )
6016 .build();
6017
6018 assert!(result.is_ok());
6019 let built_graph = result.unwrap();
6020
6021 assert_eq!(built_graph.nodegroups.len(), 1);
6024
6025 }
6029
6030 #[test]
6031 fn test_add_node_duplicate_alias_error() {
6032 let graph = create_test_graph();
6033
6034 let result = GraphMutator::new(graph)
6036 .add_string_node(
6037 Some("root"),
6038 "root", "Duplicate",
6040 Cardinality::One,
6041 "E41_Appellation",
6042 "P1_is_identified_by",
6043 None,
6044 NodeOptions::default(),
6045 None,
6046 )
6047 .build();
6048
6049 assert!(result.is_err());
6050 assert!(matches!(result, Err(MutationError::AliasAlreadyExists(_))));
6051 }
6052
6053 #[test]
6054 fn test_add_node_invalid_config_error() {
6055 let graph = create_test_graph();
6056
6057 let result = GraphMutator::new(graph)
6059 .add_generic_node(
6060 Some("root"),
6061 "child",
6062 "Child",
6063 Cardinality::N,
6064 "string",
6065 "E41_Appellation",
6066 "P1_is_identified_by",
6067 None,
6068 NodeOptions::default(),
6069 Some(serde_json::json!("not an object")), )
6071 .build();
6072
6073 assert!(result.is_err());
6074 assert!(matches!(result, Err(MutationError::InvalidConfig { .. })));
6075 }
6076
6077 #[test]
6078 fn test_get_default_widget() {
6079 assert!(get_default_widget_for_datatype("string").is_ok());
6080 assert!(get_default_widget_for_datatype("number").is_ok());
6081 assert!(get_default_widget_for_datatype("concept").is_ok());
6082 assert!(get_default_widget_for_datatype("semantic").is_err());
6083 assert!(get_default_widget_for_datatype("unknown").is_err());
6084 }
6085
6086 #[test]
6087 fn test_json_mutation_api() {
6088 let graph = create_test_graph();
6089
6090 let mutations_json = r#"{
6091 "mutations": [
6092 {
6093 "AddNode": {
6094 "parent_alias": "root",
6095 "alias": "child",
6096 "name": "Child Node",
6097 "cardinality": "N",
6098 "datatype": "string",
6099 "ontology_class": "E41_Appellation",
6100 "parent_property": "P1_is_identified_by",
6101 "description": "A test child node",
6102 "config": null,
6103 "options": {
6104 "isrequired": true
6105 }
6106 }
6107 }
6108 ],
6109 "options": {
6110 "autocreate_card": true,
6111 "autocreate_widget": true
6112 }
6113 }"#;
6114
6115 let result = apply_mutations_from_json(&graph, mutations_json);
6116 assert!(result.is_ok(), "JSON mutation failed: {:?}", result.err());
6117
6118 let mutated = result.unwrap();
6119 assert_eq!(mutated.nodes.len(), 2);
6121 assert_eq!(mutated.nodegroups.len(), 2);
6123 assert_eq!(mutated.edges.len(), 1);
6125 }
6126
6127 #[test]
6128 fn test_mutations_serialization() {
6129 let mutations = vec![GraphMutation::AddNode(AddNodeParams {
6130 parent_alias: Some("root".to_string()),
6131 alias: "test".to_string(),
6132 name: "Test".to_string(),
6133 cardinality: Cardinality::One,
6134 datatype: "string".to_string(),
6135 ontology_class: Some(vec!["E41".to_string()]),
6136 parent_property: "P1".to_string(),
6137 description: None,
6138 config: None,
6139 options: NodeOptions::default(),
6140 })];
6141
6142 let json = mutations_to_json(&mutations);
6143 assert!(json.is_ok());
6144
6145 let parsed: Result<Vec<GraphMutation>, _> = serde_json::from_str(&json.unwrap());
6147 assert!(parsed.is_ok());
6148 }
6149
6150 fn create_test_subgraph() -> StaticGraph {
6151 let subgraph_json = r#"{
6153 "graphid": "subgraph-id",
6154 "name": {"en": "Test Subgraph"},
6155 "isresource": false,
6156 "publication": {
6157 "publicationid": "test-publication-id",
6158 "graph_id": "subgraph-id",
6159 "published_time": "2024-01-01T00:00:00.000"
6160 },
6161 "nodes": [
6162 {
6163 "nodeid": "sub-root-id",
6164 "name": "Subgraph Root",
6165 "alias": "sub_root",
6166 "datatype": "semantic",
6167 "nodegroup_id": "sub-root-ng",
6168 "graph_id": "subgraph-id",
6169 "is_collector": true,
6170 "isrequired": false,
6171 "exportable": false,
6172 "ontologyclass": "E41_Appellation",
6173 "hascustomalias": false,
6174 "issearchable": false,
6175 "istopnode": true
6176 },
6177 {
6178 "nodeid": "sub-child1-id",
6179 "name": "Child 1",
6180 "alias": "child1",
6181 "datatype": "string",
6182 "nodegroup_id": "sub-child1-ng",
6183 "graph_id": "subgraph-id",
6184 "is_collector": false,
6185 "isrequired": false,
6186 "exportable": true,
6187 "ontologyclass": "E41_Appellation",
6188 "hascustomalias": false,
6189 "issearchable": true,
6190 "istopnode": false
6191 },
6192 {
6193 "nodeid": "sub-child2-id",
6194 "name": "Child 2",
6195 "alias": "child2",
6196 "datatype": "concept",
6197 "nodegroup_id": "sub-child1-ng",
6198 "graph_id": "subgraph-id",
6199 "is_collector": false,
6200 "isrequired": false,
6201 "exportable": true,
6202 "ontologyclass": "E55_Type",
6203 "hascustomalias": false,
6204 "issearchable": true,
6205 "istopnode": false
6206 }
6207 ],
6208 "nodegroups": [
6209 {
6210 "nodegroupid": "sub-root-ng",
6211 "cardinality": "n",
6212 "parentnodegroup_id": null
6213 },
6214 {
6215 "nodegroupid": "sub-child1-ng",
6216 "cardinality": "1",
6217 "parentnodegroup_id": "sub-root-ng"
6218 }
6219 ],
6220 "edges": [
6221 {
6222 "edgeid": "sub-edge1-id",
6223 "domainnode_id": "sub-root-id",
6224 "rangenode_id": "sub-child1-id",
6225 "graph_id": "subgraph-id",
6226 "ontologyproperty": "P3_has_note"
6227 },
6228 {
6229 "edgeid": "sub-edge2-id",
6230 "domainnode_id": "sub-child1-id",
6231 "rangenode_id": "sub-child2-id",
6232 "graph_id": "subgraph-id",
6233 "ontologyproperty": "P2_has_type"
6234 }
6235 ],
6236 "cards": [
6237 {
6238 "cardid": "sub-card1-id",
6239 "nodegroup_id": "sub-child1-ng",
6240 "graph_id": "subgraph-id",
6241 "name": {"en": "Child Card"},
6242 "active": true,
6243 "visible": true,
6244 "component_id": "f05e4d3a-53c1-11e8-b0ea-784f435179ea",
6245 "helpenabled": false,
6246 "helptext": {"en": ""},
6247 "helptitle": {"en": ""},
6248 "instructions": {"en": ""},
6249 "constraints": []
6250 }
6251 ],
6252 "cards_x_nodes_x_widgets": [
6253 {
6254 "id": "sub-cxnxw1-id",
6255 "card_id": "sub-card1-id",
6256 "node_id": "sub-child1-id",
6257 "widget_id": "10000000-0000-0000-0000-000000000001",
6258 "config": {},
6259 "label": {"en": "Child 1 Label"},
6260 "sortorder": 1,
6261 "visible": true
6262 },
6263 {
6264 "id": "sub-cxnxw2-id",
6265 "card_id": "sub-card1-id",
6266 "node_id": "sub-child2-id",
6267 "widget_id": "10000000-0000-0000-0000-000000000002",
6268 "config": {},
6269 "label": {"en": "Child 2 Label"},
6270 "sortorder": 2,
6271 "visible": true
6272 }
6273 ],
6274 "root": {
6275 "nodeid": "sub-root-id",
6276 "name": "Subgraph Root",
6277 "alias": "sub_root",
6278 "datatype": "semantic",
6279 "nodegroup_id": "sub-root-ng",
6280 "graph_id": "subgraph-id",
6281 "is_collector": true,
6282 "isrequired": false,
6283 "exportable": false,
6284 "ontologyclass": "E41_Appellation",
6285 "hascustomalias": false,
6286 "issearchable": false,
6287 "istopnode": true
6288 }
6289 }"#;
6290
6291 let mut graph: StaticGraph =
6292 serde_json::from_str(subgraph_json).expect("Failed to parse test subgraph JSON");
6293 graph.build_indices();
6294 graph
6295 }
6296
6297 #[test]
6298 fn test_add_subgraph_basic() {
6299 let graph = create_test_graph();
6300 let subgraph = create_test_subgraph();
6301
6302 let params = AddSubgraphParams {
6303 subgraph,
6304 target_node_id: "root-node-id".to_string(),
6305 ontology_property: "P106_is_composed_of".to_string(),
6306 alias_suffix: None,
6307 alias_prefix: None,
6308 name_prefix: None,
6309 };
6310
6311 let mut graph_clone = graph.deep_clone();
6312 let result = apply_add_subgraph(&mut graph_clone, params);
6313
6314 assert!(result.is_ok(), "AddSubgraph failed: {:?}", result.err());
6315
6316 assert_eq!(graph_clone.nodes.len(), 4); assert_eq!(graph_clone.nodegroups.len(), 3); assert_eq!(graph_clone.edges.len(), 3);
6327
6328 assert_eq!(graph_clone.cards_slice().len(), 1);
6330
6331 assert_eq!(graph_clone.cards_x_nodes_x_widgets_slice().len(), 2);
6333 }
6334
6335 #[test]
6336 fn test_add_subgraph_with_alias_suffix() {
6337 let graph = create_test_graph();
6340 let subgraph = create_test_subgraph();
6341
6342 let params = AddSubgraphParams {
6343 subgraph,
6344 target_node_id: "root-node-id".to_string(),
6345 ontology_property: "P106_is_composed_of".to_string(),
6346 alias_suffix: Some("v2".to_string()),
6347 alias_prefix: None,
6348 name_prefix: None,
6349 };
6350
6351 let mut graph_clone = graph.deep_clone();
6352 let result = apply_add_subgraph(&mut graph_clone, params);
6353
6354 assert!(
6355 result.is_ok(),
6356 "AddSubgraph with suffix failed: {:?}",
6357 result.err()
6358 );
6359
6360 let child1 = graph_clone
6362 .nodes
6363 .iter()
6364 .find(|n| n.alias.as_deref() == Some("child1"));
6365 assert!(
6366 child1.is_some(),
6367 "Node with alias 'child1' not found (aliases should be preserved when no clash)"
6368 );
6369
6370 let child2 = graph_clone
6371 .nodes
6372 .iter()
6373 .find(|n| n.alias.as_deref() == Some("child2"));
6374 assert!(
6375 child2.is_some(),
6376 "Node with alias 'child2' not found (aliases should be preserved when no clash)"
6377 );
6378
6379 let child1_node = child1.unwrap();
6381 assert!(
6382 child1_node.sourcebranchpublication_id.is_some(),
6383 "sourcebranchpublication_id should be set on branch nodes"
6384 );
6385 }
6386
6387 #[test]
6388 fn test_add_subgraph_alias_clash() {
6389 let graph = create_test_graph();
6392
6393 let mut graph_with_child = GraphMutator::new(graph)
6395 .add_string_node(
6396 Some("root"),
6397 "child1",
6398 "Existing Child",
6399 Cardinality::N,
6400 "E41_Appellation",
6401 "P1_is_identified_by",
6402 None,
6403 NodeOptions::default(),
6404 None,
6405 )
6406 .build()
6407 .expect("Failed to create graph with child");
6408
6409 let subgraph = create_test_subgraph();
6411
6412 let params = AddSubgraphParams {
6413 subgraph,
6414 target_node_id: "root-node-id".to_string(),
6415 ontology_property: "P106_is_composed_of".to_string(),
6416 alias_suffix: None,
6417 alias_prefix: None,
6418 name_prefix: None,
6419 };
6420
6421 let result = apply_add_subgraph(&mut graph_with_child, params);
6422
6423 assert!(
6425 result.is_ok(),
6426 "AddSubgraph should auto-suffix clashing aliases: {:?}",
6427 result.err()
6428 );
6429
6430 let original_child1 = graph_with_child
6432 .nodes
6433 .iter()
6434 .find(|n| n.alias.as_deref() == Some("child1") && n.name == "Existing Child");
6435 assert!(
6436 original_child1.is_some(),
6437 "Original 'child1' node should still exist"
6438 );
6439
6440 let new_child1 = graph_with_child
6442 .nodes
6443 .iter()
6444 .find(|n| n.alias.as_deref() == Some("child1_n1"));
6445 assert!(
6446 new_child1.is_some(),
6447 "Clashing alias should be renamed to 'child1_n1'"
6448 );
6449
6450 let child2 = graph_with_child
6452 .nodes
6453 .iter()
6454 .find(|n| n.alias.as_deref() == Some("child2"));
6455 assert!(
6456 child2.is_some(),
6457 "Non-clashing alias 'child2' should be preserved"
6458 );
6459 }
6460
6461 #[test]
6462 fn test_add_subgraph_id_remapping() {
6463 let graph = create_test_graph();
6464 let subgraph = create_test_subgraph();
6465
6466 let params = AddSubgraphParams {
6467 subgraph,
6468 target_node_id: "root-node-id".to_string(),
6469 ontology_property: "P106_is_composed_of".to_string(),
6470 alias_suffix: None,
6471 alias_prefix: None,
6472 name_prefix: None,
6473 };
6474
6475 let mut graph_clone = graph.deep_clone();
6476 let result = apply_add_subgraph(&mut graph_clone, params);
6477 assert!(result.is_ok());
6478
6479 let original_ids = ["sub-root-id", "sub-child1-id", "sub-child2-id"];
6481 for node in &graph_clone.nodes {
6482 assert!(
6483 !original_ids.contains(&node.nodeid.as_str()),
6484 "Node ID {} was not remapped",
6485 node.nodeid
6486 );
6487 }
6488
6489 let original_edge_ids = ["sub-edge1-id", "sub-edge2-id"];
6491 for edge in &graph_clone.edges {
6492 assert!(
6493 !original_edge_ids.contains(&edge.edgeid.as_str()),
6494 "Edge ID {} was not remapped",
6495 edge.edgeid
6496 );
6497 }
6498
6499 for node in &graph_clone.nodes {
6501 assert_eq!(
6502 node.graph_id, "test-graph-id",
6503 "Node graph_id not remapped to target graph"
6504 );
6505 }
6506 }
6507
6508 #[test]
6509 fn test_add_subgraph_preserves_external_ids() {
6510 let graph = create_test_graph();
6511 let subgraph = create_test_subgraph();
6512
6513 let params = AddSubgraphParams {
6514 subgraph,
6515 target_node_id: "root-node-id".to_string(),
6516 ontology_property: "P106_is_composed_of".to_string(),
6517 alias_suffix: None,
6518 alias_prefix: None,
6519 name_prefix: None,
6520 };
6521
6522 let mut graph_clone = graph.deep_clone();
6523 let result = apply_add_subgraph(&mut graph_clone, params);
6524 assert!(result.is_ok());
6525
6526 let cxnxws = graph_clone.cards_x_nodes_x_widgets_slice();
6528 assert!(
6529 cxnxws
6530 .iter()
6531 .any(|c| c.widget_id == "10000000-0000-0000-0000-000000000001"),
6532 "Widget ID for text-widget should be preserved"
6533 );
6534 assert!(
6535 cxnxws
6536 .iter()
6537 .any(|c| c.widget_id == "10000000-0000-0000-0000-000000000002"),
6538 "Widget ID for concept-select-widget should be preserved"
6539 );
6540
6541 let cards = graph_clone.cards_slice();
6543 assert!(
6544 cards
6545 .iter()
6546 .any(|c| c.component_id == "f05e4d3a-53c1-11e8-b0ea-784f435179ea"),
6547 "Component ID should be preserved"
6548 );
6549 }
6550
6551 #[test]
6552 fn test_add_subgraph_target_not_found() {
6553 let graph = create_test_graph();
6554 let subgraph = create_test_subgraph();
6555
6556 let params = AddSubgraphParams {
6557 subgraph,
6558 target_node_id: "nonexistent-node-id".to_string(),
6559 ontology_property: "P106_is_composed_of".to_string(),
6560 alias_suffix: None,
6561 alias_prefix: None,
6562 name_prefix: None,
6563 };
6564
6565 let mut graph_clone = graph.deep_clone();
6566 let result = apply_add_subgraph(&mut graph_clone, params);
6567
6568 assert!(result.is_err(), "Expected NodeNotFound error");
6569 match result {
6570 Err(MutationError::NodeNotFound(id)) => {
6571 assert_eq!(id, "nonexistent-node-id");
6572 }
6573 Err(e) => panic!("Expected NodeNotFound error, got: {:?}", e),
6574 Ok(_) => panic!("Expected error but got Ok"),
6575 }
6576 }
6577
6578 #[test]
6579 fn test_add_subgraph_via_json_api() {
6580 let graph = create_test_graph();
6581 let subgraph = create_test_subgraph();
6582
6583 let subgraph_json = serde_json::to_string(&subgraph).expect("Failed to serialize subgraph");
6585
6586 let mutations_json = format!(
6587 r#"{{
6588 "mutations": [
6589 {{
6590 "AddSubgraph": {{
6591 "subgraph": {},
6592 "target_node_id": "root-node-id",
6593 "ontology_property": "P106_is_composed_of",
6594 "alias_suffix": "json"
6595 }}
6596 }}
6597 ],
6598 "options": {{
6599 "autocreate_card": true,
6600 "autocreate_widget": true
6601 }}
6602 }}"#,
6603 subgraph_json
6604 );
6605
6606 let result = apply_mutations_from_json(&graph, &mutations_json);
6607 assert!(
6608 result.is_ok(),
6609 "JSON AddSubgraph mutation failed: {:?}",
6610 result.err()
6611 );
6612
6613 let mutated = result.unwrap();
6614 assert_eq!(mutated.nodes.len(), 4);
6616
6617 assert!(
6620 mutated
6621 .nodes
6622 .iter()
6623 .any(|n| n.alias.as_deref() == Some("child1")),
6624 "Alias 'child1' should be preserved (no clash)"
6625 );
6626
6627 let branch_node = mutated
6629 .nodes
6630 .iter()
6631 .find(|n| n.alias.as_deref() == Some("child1"))
6632 .unwrap();
6633 assert!(
6634 branch_node.sourcebranchpublication_id.is_some(),
6635 "sourcebranchpublication_id should be set on branch nodes"
6636 );
6637 }
6638
6639 #[test]
6644 fn test_update_subgraph_first_time_acts_like_add() {
6645 let graph = create_test_graph();
6647 let subgraph = create_test_subgraph();
6648
6649 let params = UpdateSubgraphParams {
6650 subgraph,
6651 target_node_id: "root-node-id".to_string(),
6652 ontology_property: "P106_is_composed_of".to_string(),
6653 alias_suffix: None,
6654 remove_orphaned: false,
6655 alias_prefix: None,
6656 name_prefix: None,
6657 };
6658
6659 let mut graph_clone = graph.deep_clone();
6660 let result = apply_update_subgraph(&mut graph_clone, params);
6661
6662 assert!(
6663 result.is_ok(),
6664 "UpdateSubgraph should succeed: {:?}",
6665 result.err()
6666 );
6667
6668 assert_eq!(
6670 graph_clone.nodes.len(),
6671 4,
6672 "Should have 4 nodes: root + 3 from branch (branch root kept as collector)"
6673 );
6674
6675 let child1 = graph_clone
6677 .nodes
6678 .iter()
6679 .find(|n| n.alias.as_deref() == Some("child1"))
6680 .unwrap();
6681 assert!(child1.sourcebranchpublication_id.is_some());
6682 }
6683
6684 #[test]
6685 fn test_update_subgraph_updates_existing_nodes() {
6686 let graph = create_test_graph();
6688 let subgraph = create_test_subgraph();
6689
6690 let add_params = AddSubgraphParams {
6692 subgraph: subgraph.clone(),
6693 target_node_id: "root-node-id".to_string(),
6694 ontology_property: "P106_is_composed_of".to_string(),
6695 alias_suffix: None,
6696 alias_prefix: None,
6697 name_prefix: None,
6698 };
6699 let mut graph_with_branch = graph.deep_clone();
6700 apply_add_subgraph(&mut graph_with_branch, add_params).expect("Add should succeed");
6701
6702 let mut updated_subgraph = subgraph.deep_clone();
6704 for node in &mut updated_subgraph.nodes {
6706 if node.alias.as_deref() == Some("child1") {
6707 node.name = "Updated Child 1".to_string();
6708 }
6709 }
6710
6711 let update_params = UpdateSubgraphParams {
6712 subgraph: updated_subgraph,
6713 target_node_id: "root-node-id".to_string(),
6714 ontology_property: "P106_is_composed_of".to_string(),
6715 alias_suffix: None,
6716 remove_orphaned: false,
6717 alias_prefix: None,
6718 name_prefix: None,
6719 };
6720 let result = apply_update_subgraph(&mut graph_with_branch, update_params);
6721
6722 assert!(
6723 result.is_ok(),
6724 "UpdateSubgraph should succeed: {:?}",
6725 result.err()
6726 );
6727
6728 assert_eq!(graph_with_branch.nodes.len(), 4);
6730
6731 let child1 = graph_with_branch
6733 .nodes
6734 .iter()
6735 .find(|n| n.alias.as_deref() == Some("child1"))
6736 .unwrap();
6737 assert_eq!(
6738 child1.name, "Updated Child 1",
6739 "Node name should be updated"
6740 );
6741 }
6742
6743 #[test]
6744 fn test_update_subgraph_adds_new_nodes() {
6745 let graph = create_test_graph();
6747 let subgraph = create_test_subgraph();
6748
6749 let add_params = AddSubgraphParams {
6751 subgraph: subgraph.clone(),
6752 target_node_id: "root-node-id".to_string(),
6753 ontology_property: "P106_is_composed_of".to_string(),
6754 alias_suffix: None,
6755 alias_prefix: None,
6756 name_prefix: None,
6757 };
6758 let mut graph_with_branch = graph.deep_clone();
6759 apply_add_subgraph(&mut graph_with_branch, add_params).expect("Add should succeed");
6760 assert_eq!(graph_with_branch.nodes.len(), 4);
6761
6762 let mut updated_subgraph = subgraph.deep_clone();
6764 let new_node = StaticNode {
6766 nodeid: "sub-child3-id".to_string(),
6767 name: "Child 3".to_string(),
6768 alias: Some("child3".to_string()),
6769 datatype: "string".to_string(),
6770 nodegroup_id: Some("sub-child-ng-id".to_string()),
6771 graph_id: "sub-graph-id".to_string(),
6772 is_collector: false,
6773 isrequired: false,
6774 exportable: true,
6775 sortorder: Some(3),
6776 config: HashMap::new(),
6777 parentproperty: None,
6778 ontologyclass: Some(vec!["E41_Appellation".to_string()]),
6779 description: None,
6780 fieldname: None,
6781 hascustomalias: false,
6782 issearchable: true,
6783 istopnode: false,
6784 sourcebranchpublication_id: None,
6785 source_identifier_id: None,
6786 is_immutable: None,
6787 };
6788 updated_subgraph.nodes.push(new_node);
6789
6790 let update_params = UpdateSubgraphParams {
6791 subgraph: updated_subgraph,
6792 target_node_id: "root-node-id".to_string(),
6793 ontology_property: "P106_is_composed_of".to_string(),
6794 alias_suffix: None,
6795 remove_orphaned: false,
6796 alias_prefix: None,
6797 name_prefix: None,
6798 };
6799 let result = apply_update_subgraph(&mut graph_with_branch, update_params);
6800
6801 assert!(
6802 result.is_ok(),
6803 "UpdateSubgraph should succeed: {:?}",
6804 result.err()
6805 );
6806
6807 assert_eq!(
6809 graph_with_branch.nodes.len(),
6810 5,
6811 "Should have added new node"
6812 );
6813
6814 let child3 = graph_with_branch
6816 .nodes
6817 .iter()
6818 .find(|n| n.alias.as_deref() == Some("child3"));
6819 assert!(child3.is_some(), "New node child3 should be added");
6820 }
6821
6822 #[test]
6823 fn test_update_subgraph_removes_orphaned() {
6824 let graph = create_test_graph();
6826 let subgraph = create_test_subgraph();
6827
6828 let add_params = AddSubgraphParams {
6830 subgraph: subgraph.clone(),
6831 target_node_id: "root-node-id".to_string(),
6832 ontology_property: "P106_is_composed_of".to_string(),
6833 alias_suffix: None,
6834 alias_prefix: None,
6835 name_prefix: None,
6836 };
6837 let mut graph_with_branch = graph.deep_clone();
6838 apply_add_subgraph(&mut graph_with_branch, add_params).expect("Add should succeed");
6839 assert_eq!(graph_with_branch.nodes.len(), 4);
6840
6841 let mut updated_subgraph = subgraph.deep_clone();
6843 updated_subgraph
6844 .nodes
6845 .retain(|n| n.alias.as_deref() != Some("child2"));
6846
6847 let update_params = UpdateSubgraphParams {
6848 subgraph: updated_subgraph,
6849 target_node_id: "root-node-id".to_string(),
6850 ontology_property: "P106_is_composed_of".to_string(),
6851 alias_suffix: None,
6852 remove_orphaned: true, alias_prefix: None,
6854 name_prefix: None,
6855 };
6856 let result = apply_update_subgraph(&mut graph_with_branch, update_params);
6857
6858 assert!(
6859 result.is_ok(),
6860 "UpdateSubgraph should succeed: {:?}",
6861 result.err()
6862 );
6863
6864 assert_eq!(
6867 graph_with_branch.nodes.len(),
6868 2,
6869 "Orphaned child2 and branch root should be removed"
6870 );
6871
6872 let child2 = graph_with_branch
6874 .nodes
6875 .iter()
6876 .find(|n| n.alias.as_deref() == Some("child2"));
6877 assert!(child2.is_none(), "child2 should be removed");
6878 }
6879
6880 #[test]
6881 fn test_update_subgraph_target_not_found() {
6882 let graph = create_test_graph();
6883 let subgraph = create_test_subgraph();
6884
6885 let params = UpdateSubgraphParams {
6886 subgraph,
6887 target_node_id: "non-existent-node".to_string(),
6888 ontology_property: "P106_is_composed_of".to_string(),
6889 alias_suffix: None,
6890 remove_orphaned: false,
6891 alias_prefix: None,
6892 name_prefix: None,
6893 };
6894
6895 let mut graph_clone = graph.deep_clone();
6896 let result = apply_update_subgraph(&mut graph_clone, params);
6897
6898 assert!(result.is_err(), "Should fail when target not found");
6899 match result {
6900 Err(MutationError::NodeNotFound(id)) => {
6901 assert_eq!(id, "non-existent-node");
6902 }
6903 Err(e) => panic!("Expected NodeNotFound error, got: {:?}", e),
6904 Ok(_) => panic!("Expected error but got Ok"),
6905 }
6906 }
6907
6908 #[test]
6913 fn test_concept_change_collection_concept_node() {
6914 let mut graph = create_test_graph();
6916
6917 let concept_node = StaticNode {
6919 nodeid: "concept-node-id".to_string(),
6920 name: "Test Concept".to_string(),
6921 alias: Some("test_concept".to_string()),
6922 datatype: "concept".to_string(),
6923 nodegroup_id: Some("root-node-id".to_string()),
6924 graph_id: "test-graph-id".to_string(),
6925 is_collector: false,
6926 isrequired: false,
6927 exportable: true,
6928 sortorder: Some(1),
6929 config: HashMap::new(),
6930 parentproperty: None,
6931 ontologyclass: Some(vec!["E55_Type".to_string()]),
6932 description: None,
6933 fieldname: None,
6934 hascustomalias: false,
6935 issearchable: true,
6936 istopnode: false,
6937 sourcebranchpublication_id: None,
6938 source_identifier_id: None,
6939 is_immutable: None,
6940 };
6941 graph.push_node(concept_node);
6942
6943 let params = ConceptChangeCollectionParams {
6944 node_id: "test_concept".to_string(),
6945 collection_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
6946 };
6947
6948 let result = apply_concept_change_collection(&mut graph, params);
6949 assert!(
6950 result.is_ok(),
6951 "ConceptChangeCollection should succeed: {:?}",
6952 result.err()
6953 );
6954
6955 let node = graph.find_node_by_alias("test_concept").unwrap();
6957 let rdm_collection = node.config.get("rdmCollection").and_then(|v| v.as_str());
6958 assert_eq!(rdm_collection, Some("550e8400-e29b-41d4-a716-446655440000"));
6959 }
6960
6961 #[test]
6962 fn test_concept_change_collection_concept_list_node() {
6963 let mut graph = create_test_graph();
6964
6965 let concept_list_node = StaticNode {
6967 nodeid: "concept-list-node-id".to_string(),
6968 name: "Test Concept List".to_string(),
6969 alias: Some("test_concept_list".to_string()),
6970 datatype: "concept-list".to_string(),
6971 nodegroup_id: Some("root-node-id".to_string()),
6972 graph_id: "test-graph-id".to_string(),
6973 is_collector: false,
6974 isrequired: false,
6975 exportable: true,
6976 sortorder: Some(1),
6977 config: HashMap::new(),
6978 parentproperty: None,
6979 ontologyclass: Some(vec!["E55_Type".to_string()]),
6980 description: None,
6981 fieldname: None,
6982 hascustomalias: false,
6983 issearchable: true,
6984 istopnode: false,
6985 sourcebranchpublication_id: None,
6986 source_identifier_id: None,
6987 is_immutable: None,
6988 };
6989 graph.push_node(concept_list_node);
6990
6991 let params = ConceptChangeCollectionParams {
6992 node_id: "test_concept_list".to_string(),
6993 collection_id: "my-new-collection-id".to_string(),
6994 };
6995
6996 let result = apply_concept_change_collection(&mut graph, params);
6997 assert!(
6998 result.is_ok(),
6999 "ConceptChangeCollection should succeed for concept-list: {:?}",
7000 result.err()
7001 );
7002
7003 let node = graph.find_node_by_alias("test_concept_list").unwrap();
7004 assert_eq!(
7005 node.config.get("rdmCollection").and_then(|v| v.as_str()),
7006 Some("my-new-collection-id")
7007 );
7008 }
7009
7010 #[test]
7011 fn test_concept_change_collection_invalid_datatype() {
7012 let mut graph = create_test_graph();
7013
7014 let string_node = StaticNode {
7016 nodeid: "string-node-id".to_string(),
7017 name: "Test String".to_string(),
7018 alias: Some("test_string".to_string()),
7019 datatype: "string".to_string(),
7020 nodegroup_id: Some("root-node-id".to_string()),
7021 graph_id: "test-graph-id".to_string(),
7022 is_collector: false,
7023 isrequired: false,
7024 exportable: true,
7025 sortorder: Some(1),
7026 config: HashMap::new(),
7027 parentproperty: None,
7028 ontologyclass: Some(vec!["E41_Appellation".to_string()]),
7029 description: None,
7030 fieldname: None,
7031 hascustomalias: false,
7032 issearchable: true,
7033 istopnode: false,
7034 sourcebranchpublication_id: None,
7035 source_identifier_id: None,
7036 is_immutable: None,
7037 };
7038 graph.push_node(string_node);
7039
7040 let params = ConceptChangeCollectionParams {
7041 node_id: "test_string".to_string(),
7042 collection_id: "some-collection".to_string(),
7043 };
7044
7045 let result = apply_concept_change_collection(&mut graph, params);
7046 assert!(result.is_err(), "Should fail for non-concept datatype");
7047
7048 match result {
7049 Err(MutationError::InvalidDatatype {
7050 expected,
7051 found,
7052 node_id,
7053 }) => {
7054 assert!(expected.contains("concept"));
7055 assert_eq!(found, "string");
7056 assert_eq!(node_id, "test_string");
7057 }
7058 Err(e) => panic!("Expected InvalidDatatype error, got: {:?}", e),
7059 Ok(_) => panic!("Expected error but got Ok"),
7060 }
7061 }
7062
7063 #[test]
7064 fn test_concept_change_collection_node_not_found() {
7065 let mut graph = create_test_graph();
7066
7067 let params = ConceptChangeCollectionParams {
7068 node_id: "nonexistent_node".to_string(),
7069 collection_id: "some-collection".to_string(),
7070 };
7071
7072 let result = apply_concept_change_collection(&mut graph, params);
7073 assert!(result.is_err(), "Should fail when node not found");
7074
7075 match result {
7076 Err(MutationError::NodeNotFound(id)) => {
7077 assert_eq!(id, "nonexistent_node");
7078 }
7079 Err(e) => panic!("Expected NodeNotFound error, got: {:?}", e),
7080 Ok(_) => panic!("Expected error but got Ok"),
7081 }
7082 }
7083
7084 #[test]
7085 fn test_concept_change_collection_by_node_id() {
7086 let mut graph = create_test_graph();
7088
7089 let concept_node = StaticNode {
7090 nodeid: "concept-node-uuid".to_string(),
7091 name: "Test Concept".to_string(),
7092 alias: None, datatype: "concept".to_string(),
7094 nodegroup_id: Some("root-node-id".to_string()),
7095 graph_id: "test-graph-id".to_string(),
7096 is_collector: false,
7097 isrequired: false,
7098 exportable: true,
7099 sortorder: Some(1),
7100 config: HashMap::new(),
7101 parentproperty: None,
7102 ontologyclass: Some(vec!["E55_Type".to_string()]),
7103 description: None,
7104 fieldname: None,
7105 hascustomalias: false,
7106 issearchable: true,
7107 istopnode: false,
7108 sourcebranchpublication_id: None,
7109 source_identifier_id: None,
7110 is_immutable: None,
7111 };
7112 graph.push_node(concept_node);
7113
7114 let params = ConceptChangeCollectionParams {
7115 node_id: "concept-node-uuid".to_string(), collection_id: "new-collection".to_string(),
7117 };
7118
7119 let result = apply_concept_change_collection(&mut graph, params);
7120 assert!(result.is_ok(), "Should find node by ID: {:?}", result.err());
7121
7122 let node = graph
7123 .nodes
7124 .iter()
7125 .find(|n| n.nodeid == "concept-node-uuid")
7126 .unwrap();
7127 assert_eq!(
7128 node.config.get("rdmCollection").and_then(|v| v.as_str()),
7129 Some("new-collection")
7130 );
7131 }
7132
7133 #[test]
7134 fn test_concept_change_collection_via_instruction() {
7135 let mut graph = create_test_graph();
7136
7137 let concept_node = StaticNode {
7139 nodeid: "concept-node-id".to_string(),
7140 name: "Test Concept".to_string(),
7141 alias: Some("my_concept".to_string()),
7142 datatype: "concept".to_string(),
7143 nodegroup_id: Some("root-node-id".to_string()),
7144 graph_id: "test-graph-id".to_string(),
7145 is_collector: false,
7146 isrequired: false,
7147 exportable: true,
7148 sortorder: Some(1),
7149 config: HashMap::new(),
7150 parentproperty: None,
7151 ontologyclass: Some(vec!["E55_Type".to_string()]),
7152 description: None,
7153 fieldname: None,
7154 hascustomalias: false,
7155 issearchable: true,
7156 istopnode: false,
7157 sourcebranchpublication_id: None,
7158 source_identifier_id: None,
7159 is_immutable: None,
7160 };
7161 graph.push_node(concept_node);
7162
7163 let instruction = GraphInstruction {
7165 action: "concept_change_collection".to_string(),
7166 subject: "my_concept".to_string(),
7167 object: "new-collection-uuid".to_string(),
7168 params: HashMap::new(),
7169 };
7170
7171 let mutation = instruction.to_mutation().expect("Should create mutation");
7172 let options = MutatorOptions::default();
7173 let result = apply_mutation(&mut graph, mutation, &options);
7174 assert!(
7175 result.is_ok(),
7176 "Instruction should apply: {:?}",
7177 result.err()
7178 );
7179
7180 let node = graph.find_node_by_alias("my_concept").unwrap();
7181 assert_eq!(
7182 node.config.get("rdmCollection").and_then(|v| v.as_str()),
7183 Some("new-collection-uuid")
7184 );
7185 }
7186
7187 #[test]
7192 fn test_create_skeleton_graph() {
7193 let classes = vec!["http://example.org/Person".to_string()];
7194 let graph = create_skeleton_graph("Person", "person", true, Some(classes.as_slice()));
7195
7196 assert!(!graph.graphid.is_empty());
7198 assert_eq!(graph.name.to_string_default(), "Person".to_string());
7199 assert_eq!(graph.isresource, Some(true));
7200
7201 assert_eq!(graph.root.alias, Some("person".to_string()));
7203 assert_eq!(graph.root.datatype, "semantic");
7204 assert!(graph.root.istopnode);
7205 assert!(
7206 graph.root.nodegroup_id.is_none(),
7207 "Root should have no nodegroup"
7208 );
7209 assert_eq!(
7210 graph.root.ontologyclass,
7211 Some(vec!["http://example.org/Person".to_string()])
7212 );
7213
7214 assert_eq!(graph.nodes.len(), 1);
7216 assert_eq!(graph.nodes[0].nodeid, graph.root.nodeid);
7217
7218 assert!(graph.nodegroups.is_empty());
7220 assert!(graph.edges.is_empty());
7221 }
7222
7223 #[test]
7224 fn test_skeleton_graph_deterministic_ids() {
7225 let graph1 = create_skeleton_graph("Person", "person", true, None);
7226 let graph2 = create_skeleton_graph("Person", "person", true, None);
7227
7228 assert_eq!(graph1.graphid, graph2.graphid);
7230 assert_eq!(graph1.root.nodeid, graph2.root.nodeid);
7231
7232 let graph3 = create_skeleton_graph("Monument", "monument", true, None);
7234 assert_ne!(graph1.graphid, graph3.graphid);
7235 }
7236
7237 #[test]
7238 fn test_skeleton_graph_branch_vs_resource() {
7239 let resource = create_skeleton_graph("Person", "person", true, None);
7240 let branch = create_skeleton_graph("Addresses", "addresses", false, None);
7241
7242 assert_eq!(resource.isresource, Some(true));
7243 assert_eq!(branch.isresource, Some(false));
7244 }
7245
7246 #[test]
7251 fn test_instruction_add_node() {
7252 let graph = create_skeleton_graph("Person", "person", true, None);
7253
7254 let instructions = vec![GraphInstruction::new("add_node", "person", "name")
7255 .with_str("datatype", "string")
7256 .with_str("name", "Full Name")
7257 .with_str("cardinality", "n")
7258 .with_str("ontology_class", "http://example.org/Name")
7259 .with_str("parent_property", "http://example.org/hasName")];
7260
7261 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7262 assert!(result.is_ok(), "Instruction failed: {:?}", result.err());
7263
7264 let mutated = result.unwrap();
7265 assert_eq!(mutated.nodes.len(), 2);
7266
7267 let name_node = mutated
7268 .nodes
7269 .iter()
7270 .find(|n| n.alias.as_deref() == Some("name"));
7271 assert!(name_node.is_some(), "Should find 'name' node");
7272
7273 let name_node = name_node.unwrap();
7274 assert_eq!(name_node.datatype, "string");
7275 assert_eq!(name_node.name, "Full Name");
7276 assert!(
7277 name_node.nodegroup_id.is_some(),
7278 "Non-root node should have nodegroup"
7279 );
7280 }
7281
7282 #[test]
7283 fn test_instruction_multiple_nodes() {
7284 let graph = create_skeleton_graph("Person", "person", true, None);
7285
7286 let instructions = vec![
7287 GraphInstruction::new("add_node", "person", "names")
7288 .with_str("datatype", "semantic")
7289 .with_str("cardinality", "n"),
7290 GraphInstruction::new("add_node", "names", "full_name")
7291 .with_str("datatype", "string")
7292 .with_str("cardinality", "1"),
7293 GraphInstruction::new("add_node", "names", "alias_name")
7294 .with_str("datatype", "string")
7295 .with_str("cardinality", "1"),
7296 ];
7297
7298 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7299 assert!(result.is_ok(), "Instructions failed: {:?}", result.err());
7300
7301 let mutated = result.unwrap();
7302 assert_eq!(mutated.nodes.len(), 4); let names_node = mutated
7306 .nodes
7307 .iter()
7308 .find(|n| n.alias.as_deref() == Some("names"))
7309 .unwrap();
7310 assert!(names_node.nodegroup_id.is_some());
7311 let names_ng = names_node.nodegroup_id.clone().unwrap();
7312
7313 let full_name = mutated
7315 .nodes
7316 .iter()
7317 .find(|n| n.alias.as_deref() == Some("full_name"))
7318 .unwrap();
7319 let alias_name = mutated
7320 .nodes
7321 .iter()
7322 .find(|n| n.alias.as_deref() == Some("alias_name"))
7323 .unwrap();
7324 assert_eq!(full_name.nodegroup_id, Some(names_ng.clone()));
7325 assert_eq!(alias_name.nodegroup_id, Some(names_ng.clone()));
7326 }
7327
7328 #[test]
7329 fn test_instruction_from_json() {
7330 let graph = create_skeleton_graph("Person", "person", true, None);
7331
7332 let json = r#"{
7333 "instructions": [
7334 {
7335 "action": "add_node",
7336 "subject": "person",
7337 "object": "name",
7338 "params": {
7339 "datatype": "string",
7340 "cardinality": "n"
7341 }
7342 }
7343 ],
7344 "options": {
7345 "autocreate_card": true,
7346 "autocreate_widget": true
7347 }
7348 }"#;
7349
7350 let result = apply_instructions_from_json(&graph, json);
7351 assert!(
7352 result.is_ok(),
7353 "JSON instructions failed: {:?}",
7354 result.err()
7355 );
7356
7357 let mutated = result.unwrap();
7358 assert_eq!(mutated.nodes.len(), 2);
7359 }
7360
7361 #[test]
7362 fn test_instruction_unknown_action_becomes_extension() {
7363 let graph = create_skeleton_graph("Test", "test", true, None);
7364
7365 let instructions = vec![GraphInstruction::new("invalid_action", "test", "foo")];
7366
7367 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7370 assert!(result.is_err());
7371 assert!(result.unwrap_err().contains("Extension mutation"));
7372 }
7373
7374 #[test]
7375 fn test_root_children_always_get_nodegroup() {
7376 let graph = create_skeleton_graph("Test", "test", true, None);
7379
7380 let instructions = vec![
7381 GraphInstruction::new("add_node", "test", "child")
7383 .with_str("datatype", "string")
7384 .with_str("cardinality", "1"),
7385 ];
7386
7387 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7388 assert!(result.is_ok());
7389
7390 let mutated = result.unwrap();
7391 let child = mutated
7392 .nodes
7393 .iter()
7394 .find(|n| n.alias.as_deref() == Some("child"))
7395 .unwrap();
7396
7397 assert!(
7399 child.nodegroup_id.is_some(),
7400 "Direct children of root must have their own nodegroup"
7401 );
7402 }
7403
7404 #[test]
7405 fn test_is_collector_option_creates_own_nodegroup() {
7406 let graph = create_skeleton_graph("Test", "test", true, None);
7409
7410 let instructions = vec![
7411 GraphInstruction::new("add_node", "test", "parent_group")
7413 .with_str("datatype", "semantic")
7414 .with_str("cardinality", "n"),
7415 GraphInstruction::new("add_node", "parent_group", "collector_child")
7417 .with_str("datatype", "string")
7418 .with_str("cardinality", "1")
7419 .with_param("options", serde_json::json!({"is_collector": true})),
7420 GraphInstruction::new("add_node", "parent_group", "plain_child")
7422 .with_str("datatype", "string")
7423 .with_str("cardinality", "1"),
7424 ];
7425
7426 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7427 assert!(result.is_ok(), "Instructions failed: {:?}", result.err());
7428
7429 let mutated = result.unwrap();
7430 let parent = mutated
7431 .nodes
7432 .iter()
7433 .find(|n| n.alias.as_deref() == Some("parent_group"))
7434 .unwrap();
7435 let collector = mutated
7436 .nodes
7437 .iter()
7438 .find(|n| n.alias.as_deref() == Some("collector_child"))
7439 .unwrap();
7440 let plain = mutated
7441 .nodes
7442 .iter()
7443 .find(|n| n.alias.as_deref() == Some("plain_child"))
7444 .unwrap();
7445
7446 assert_eq!(
7448 collector.nodegroup_id.as_ref(),
7449 Some(&collector.nodeid),
7450 "is_collector node should have nodegroup_id == nodeid"
7451 );
7452 assert!(collector.is_collector, "is_collector flag should be set");
7453
7454 let collector_ng = mutated
7456 .nodegroups
7457 .iter()
7458 .find(|ng| ng.nodegroupid == collector.nodeid);
7459 assert!(
7460 collector_ng.is_some(),
7461 "Collector node must have a nodegroup entry"
7462 );
7463 assert_eq!(
7464 collector_ng.unwrap().parentnodegroup_id,
7465 parent.nodegroup_id,
7466 "Collector nodegroup parent should be the parent's nodegroup"
7467 );
7468
7469 assert_eq!(
7471 plain.nodegroup_id, parent.nodegroup_id,
7472 "Non-collector child should share parent's nodegroup"
7473 );
7474 }
7475
7476 #[test]
7477 fn test_is_collector_via_csv_dot_notation() {
7478 let graph = create_skeleton_graph("Test", "test", true, None);
7480
7481 let csv = "\
7482action,subject,object,params.name,params.datatype,params.cardinality,params.ontology_class,params.parent_property,params.options.is_collector
7483add_node,test,parent_group,Parent,semantic,n,,,
7484add_node,parent_group,impact,Impact,string,1,,,true
7485add_node,parent_group,other,Other,string,1,,,
7486";
7487 let instructions = parse_instructions_from_csv(csv).expect("CSV should parse");
7488 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7489 assert!(
7490 result.is_ok(),
7491 "CSV instructions failed: {:?}",
7492 result.err()
7493 );
7494
7495 let mutated = result.unwrap();
7496 let impact = mutated
7497 .nodes
7498 .iter()
7499 .find(|n| n.alias.as_deref() == Some("impact"))
7500 .unwrap();
7501 let other = mutated
7502 .nodes
7503 .iter()
7504 .find(|n| n.alias.as_deref() == Some("other"))
7505 .unwrap();
7506 let parent = mutated
7507 .nodes
7508 .iter()
7509 .find(|n| n.alias.as_deref() == Some("parent_group"))
7510 .unwrap();
7511
7512 assert_eq!(
7513 impact.nodegroup_id.as_ref(),
7514 Some(&impact.nodeid),
7515 "CSV is_collector node should have nodegroup_id == nodeid"
7516 );
7517 assert_eq!(
7518 other.nodegroup_id, parent.nodegroup_id,
7519 "Non-collector CSV node should share parent's nodegroup"
7520 );
7521 }
7522
7523 #[test]
7528 fn test_create_model_instruction() {
7529 let instructions = vec![GraphInstruction::new("create_model", "person", "")
7530 .with_str("name", "Person")
7531 .with_str("ontology_class", "http://example.org/Person")
7532 .with_str("slug", "person")];
7533
7534 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7535 assert!(result.is_ok(), "create_model failed: {:?}", result.err());
7536
7537 let graph = result.unwrap();
7538 assert_eq!(graph.isresource, Some(true));
7539 assert_eq!(graph.name.to_string_default(), "Person");
7540 assert_eq!(graph.root.alias, Some("person".to_string()));
7541 assert_eq!(
7542 graph.root.ontologyclass,
7543 Some(vec!["http://example.org/Person".to_string()])
7544 );
7545 assert_eq!(graph.slug, Some("person".to_string()));
7546 assert!(
7547 graph.root.nodegroup_id.is_none(),
7548 "Root should have no nodegroup"
7549 );
7550 }
7551
7552 #[test]
7553 fn test_create_model_default_slug() {
7554 let instructions =
7555 vec![GraphInstruction::new("create_model", "Person", "").with_str("name", "Person")];
7556
7557 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7558 assert!(result.is_ok());
7559
7560 let graph = result.unwrap();
7561 assert_eq!(graph.slug, Some("person".to_string()));
7563 }
7564
7565 #[test]
7566 fn test_create_branch_instruction() {
7567 let instructions = vec![GraphInstruction::new("create_branch", "addresses", "")
7568 .with_str("name", "Addresses")
7569 .with_str("slug", "addresses-branch")];
7570
7571 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7572 assert!(result.is_ok(), "create_branch failed: {:?}", result.err());
7573
7574 let graph = result.unwrap();
7575 assert_eq!(graph.isresource, Some(false));
7576 assert_eq!(graph.name.to_string_default(), "Addresses");
7577 assert_eq!(graph.root.alias, Some("addresses".to_string()));
7578 assert_eq!(graph.slug, Some("addresses-branch".to_string()));
7579 }
7580
7581 #[test]
7582 fn test_create_branch_default_slug() {
7583 let instructions = vec![GraphInstruction::new("create_branch", "MyAddresses", "")
7584 .with_str("name", "My Addresses")];
7585
7586 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7587 assert!(result.is_ok());
7588
7589 let graph = result.unwrap();
7590 assert_eq!(graph.slug, Some("myaddresses".to_string()));
7592 }
7593
7594 #[test]
7595 fn test_create_with_explicit_graphid() {
7596 let custom_graphid = "12345678-1234-1234-1234-123456789abc";
7597 let instructions = vec![
7598 GraphInstruction::new("create_model", "person", custom_graphid)
7599 .with_str("name", "Person"),
7600 ];
7601
7602 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7603 assert!(result.is_ok());
7604
7605 let graph = result.unwrap();
7606 assert_eq!(graph.graphid, custom_graphid);
7607 assert_eq!(graph.root.graph_id, custom_graphid);
7608 }
7609
7610 #[test]
7611 fn test_build_graph_with_nodes() {
7612 let instructions = vec![
7613 GraphInstruction::new("create_model", "person", "").with_str("name", "Person"),
7614 GraphInstruction::new("add_node", "person", "names")
7615 .with_str("datatype", "semantic")
7616 .with_str("cardinality", "n"),
7617 GraphInstruction::new("add_node", "names", "full_name")
7618 .with_str("datatype", "string")
7619 .with_str("cardinality", "1"),
7620 ];
7621
7622 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7623 assert!(result.is_ok(), "Build failed: {:?}", result.err());
7624
7625 let graph = result.unwrap();
7626 assert_eq!(graph.nodes.len(), 3); let names = graph
7630 .nodes
7631 .iter()
7632 .find(|n| n.alias.as_deref() == Some("names"))
7633 .unwrap();
7634 let full_name = graph
7635 .nodes
7636 .iter()
7637 .find(|n| n.alias.as_deref() == Some("full_name"))
7638 .unwrap();
7639
7640 assert!(names.nodegroup_id.is_some());
7641 assert_eq!(full_name.nodegroup_id, names.nodegroup_id);
7642 }
7643
7644 #[test]
7645 fn test_build_graph_from_json() {
7646 let json = r#"{
7647 "instructions": [
7648 {
7649 "action": "create_model",
7650 "subject": "monument",
7651 "object": "",
7652 "params": { "name": "Monument" }
7653 },
7654 {
7655 "action": "add_node",
7656 "subject": "monument",
7657 "object": "name",
7658 "params": { "datatype": "string", "cardinality": "n" }
7659 }
7660 ],
7661 "options": {
7662 "autocreate_card": true,
7663 "autocreate_widget": true
7664 }
7665 }"#;
7666
7667 let result = build_graph_from_instructions_json(json);
7668 assert!(result.is_ok(), "JSON build failed: {:?}", result.err());
7669
7670 let graph = result.unwrap();
7671 assert_eq!(graph.isresource, Some(true));
7672 assert_eq!(graph.nodes.len(), 2);
7673 }
7674
7675 #[test]
7676 fn test_build_graph_requires_create_first() {
7677 let instructions = vec![
7678 GraphInstruction::new("add_node", "person", "name").with_str("datatype", "string"),
7680 ];
7681
7682 let result = build_graph_from_instructions(instructions, MutatorOptions::default());
7683 assert!(result.is_err());
7684 assert!(result.unwrap_err().contains("First instruction must be"));
7685 }
7686
7687 #[test]
7688 fn test_build_graph_empty_instructions() {
7689 let result = build_graph_from_instructions(vec![], MutatorOptions::default());
7690 assert!(result.is_err());
7691 assert!(result.unwrap_err().contains("No instructions provided"));
7692 }
7693
7694 #[test]
7695 fn test_create_action_in_apply_instructions_errors() {
7696 let graph = create_skeleton_graph("Test", "test", true, None);
7698
7699 let instructions = vec![GraphInstruction::new("create_model", "other", "")];
7700
7701 let result = apply_instructions(&graph, instructions, MutatorOptions::default(), None);
7702 assert!(result.is_err());
7703 assert!(result.unwrap_err().contains("creates a new graph"));
7704 }
7705
7706 #[test]
7711 fn test_delete_card() {
7712 let mut graph = create_skeleton_graph("Test", "test", false, None);
7714 let options = MutatorOptions::default();
7715
7716 apply_mutation(
7718 &mut graph,
7719 GraphMutation::AddNode(AddNodeParams {
7720 parent_alias: Some("test".to_string()),
7721 alias: "field1".to_string(),
7722 name: "Field 1".to_string(),
7723 cardinality: Cardinality::N,
7724 datatype: "string".to_string(),
7725 ontology_class: None,
7726 parent_property: String::new(),
7727 description: None,
7728 config: None,
7729 options: NodeOptions::default(),
7730 }),
7731 &options,
7732 )
7733 .unwrap();
7734
7735 let card_id = graph.cards.as_ref().unwrap()[0].cardid.clone();
7737 assert!(!card_id.is_empty());
7738
7739 assert!(graph
7741 .cards_x_nodes_x_widgets
7742 .as_ref()
7743 .map(|c| !c.is_empty())
7744 .unwrap_or(false));
7745
7746 apply_mutation(
7748 &mut graph,
7749 GraphMutation::DeleteCard(DeleteCardParams {
7750 card_id: card_id.clone(),
7751 }),
7752 &options,
7753 )
7754 .unwrap();
7755
7756 assert!(graph
7758 .cards
7759 .as_ref()
7760 .map(|c| c.iter().all(|card| card.cardid != card_id))
7761 .unwrap_or(true));
7762
7763 assert!(graph
7765 .cards_x_nodes_x_widgets
7766 .as_ref()
7767 .map(|c| c.iter().all(|w| w.card_id != card_id))
7768 .unwrap_or(true));
7769 }
7770
7771 #[test]
7772 fn test_delete_card_not_found() {
7773 let mut graph = create_skeleton_graph("Test", "test", false, None);
7774 let options = MutatorOptions::default();
7775
7776 let result = apply_mutation(
7777 &mut graph,
7778 GraphMutation::DeleteCard(DeleteCardParams {
7779 card_id: "nonexistent".to_string(),
7780 }),
7781 &options,
7782 );
7783
7784 assert!(matches!(result, Err(MutationError::CardNotFound(_))));
7785 }
7786
7787 #[test]
7788 fn test_rename_card_by_nodegroup_id() {
7789 let mut graph = create_skeleton_graph("Test", "test", false, None);
7790 let options = MutatorOptions::default();
7791
7792 apply_mutation(
7794 &mut graph,
7795 GraphMutation::AddNode(AddNodeParams {
7796 parent_alias: Some("test".to_string()),
7797 alias: "my_field".to_string(),
7798 name: "My Field".to_string(),
7799 cardinality: Cardinality::N,
7800 datatype: "string".to_string(),
7801 ontology_class: None,
7802 parent_property: String::new(),
7803 description: None,
7804 config: None,
7805 options: NodeOptions::default(),
7806 }),
7807 &options,
7808 )
7809 .unwrap();
7810
7811 let node = graph.find_node_by_alias("my_field").unwrap();
7813 let ng_id = node.nodegroup_id.clone().unwrap();
7814 let card = graph.find_card_by_nodegroup(&ng_id).unwrap();
7815 assert_eq!(card.name.get("en"), "My Field");
7816
7817 apply_mutation(
7819 &mut graph,
7820 GraphMutation::RenameCard(RenameCardParams {
7821 card_id: ng_id.clone(),
7822 language: None,
7823 name: Some("Renamed Card".to_string()),
7824 name_i18n: None,
7825 description: Some("A description".to_string()),
7826 description_i18n: None,
7827 }),
7828 &options,
7829 )
7830 .unwrap();
7831
7832 let card = graph.find_card_by_nodegroup(&ng_id).unwrap();
7833 assert_eq!(card.name.get("en"), "Renamed Card");
7834 assert_eq!(
7835 card.description.as_ref().unwrap().get("en"),
7836 "A description"
7837 );
7838 }
7839
7840 #[test]
7841 fn test_rename_card_multilingual() {
7842 let mut graph = create_skeleton_graph("Test", "test", false, None);
7843 let options = MutatorOptions::default();
7844
7845 apply_mutation(
7846 &mut graph,
7847 GraphMutation::AddNode(AddNodeParams {
7848 parent_alias: Some("test".to_string()),
7849 alias: "my_field".to_string(),
7850 name: "My Field".to_string(),
7851 cardinality: Cardinality::N,
7852 datatype: "string".to_string(),
7853 ontology_class: None,
7854 parent_property: String::new(),
7855 description: None,
7856 config: None,
7857 options: NodeOptions::default(),
7858 }),
7859 &options,
7860 )
7861 .unwrap();
7862
7863 let node = graph.find_node_by_alias("my_field").unwrap();
7864 let ng_id = node.nodegroup_id.clone().unwrap();
7865
7866 let mut name_map = HashMap::new();
7868 name_map.insert("en".to_string(), "English Name".to_string());
7869 name_map.insert("fr".to_string(), "Nom Français".to_string());
7870
7871 apply_mutation(
7872 &mut graph,
7873 GraphMutation::RenameCard(RenameCardParams {
7874 card_id: ng_id.clone(),
7875 language: None,
7876 name: None,
7877 name_i18n: Some(name_map),
7878 description: None,
7879 description_i18n: None,
7880 }),
7881 &options,
7882 )
7883 .unwrap();
7884
7885 let card = graph.find_card_by_nodegroup(&ng_id).unwrap();
7886 assert_eq!(card.name.get("en"), "English Name");
7887 assert_eq!(card.name.get("fr"), "Nom Français");
7888 }
7889
7890 #[test]
7891 fn test_rename_card_specific_language() {
7892 let mut graph = create_skeleton_graph("Test", "test", false, None);
7893 let options = MutatorOptions::default();
7894
7895 apply_mutation(
7896 &mut graph,
7897 GraphMutation::AddNode(AddNodeParams {
7898 parent_alias: Some("test".to_string()),
7899 alias: "my_field".to_string(),
7900 name: "My Field".to_string(),
7901 cardinality: Cardinality::N,
7902 datatype: "string".to_string(),
7903 ontology_class: None,
7904 parent_property: String::new(),
7905 description: None,
7906 config: None,
7907 options: NodeOptions::default(),
7908 }),
7909 &options,
7910 )
7911 .unwrap();
7912
7913 let node = graph.find_node_by_alias("my_field").unwrap();
7914 let ng_id = node.nodegroup_id.clone().unwrap();
7915
7916 apply_mutation(
7918 &mut graph,
7919 GraphMutation::RenameCard(RenameCardParams {
7920 card_id: ng_id.clone(),
7921 language: Some("fr".to_string()),
7922 name: Some("Mon Champ".to_string()),
7923 name_i18n: None,
7924 description: None,
7925 description_i18n: None,
7926 }),
7927 &options,
7928 )
7929 .unwrap();
7930
7931 let card = graph.find_card_by_nodegroup(&ng_id).unwrap();
7932 assert_eq!(card.name.get("en"), "My Field");
7933 assert_eq!(card.name.get("fr"), "Mon Champ");
7934 }
7935
7936 #[test]
7937 fn test_rename_card_not_found() {
7938 let mut graph = create_skeleton_graph("Test", "test", false, None);
7939 let options = MutatorOptions::default();
7940
7941 let result = apply_mutation(
7942 &mut graph,
7943 GraphMutation::RenameCard(RenameCardParams {
7944 card_id: "nonexistent".to_string(),
7945 language: None,
7946 name: Some("Whatever".to_string()),
7947 name_i18n: None,
7948 description: None,
7949 description_i18n: None,
7950 }),
7951 &options,
7952 );
7953
7954 assert!(matches!(result, Err(MutationError::CardNotFound(_))));
7955 }
7956
7957 #[test]
7958 fn test_realign_card_from_node() {
7959 let mut graph = create_skeleton_graph("Test", "test", false, None);
7960 let options = MutatorOptions::default();
7961
7962 apply_mutation(
7964 &mut graph,
7965 GraphMutation::AddNode(AddNodeParams {
7966 parent_alias: Some("test".to_string()),
7967 alias: "my_field".to_string(),
7968 name: "Original Name".to_string(),
7969 cardinality: Cardinality::N,
7970 datatype: "string".to_string(),
7971 ontology_class: None,
7972 parent_property: String::new(),
7973 description: None,
7974 config: None,
7975 options: NodeOptions::default(),
7976 }),
7977 &options,
7978 )
7979 .unwrap();
7980
7981 let node = graph.find_node_by_alias("my_field").unwrap();
7983 let ng_id = node.nodegroup_id.clone().unwrap();
7984 let node_id = node.nodeid.clone();
7985 assert_eq!(
7986 graph.find_card_by_nodegroup(&ng_id).unwrap().name.get("en"),
7987 "Original Name"
7988 );
7989 let widget = graph
7990 .cards_x_nodes_x_widgets
7991 .as_ref()
7992 .unwrap()
7993 .iter()
7994 .find(|c| c.node_id == node_id)
7995 .unwrap();
7996 assert_eq!(widget.label.get("en"), "Original Name");
7997
7998 apply_mutation(
8000 &mut graph,
8001 GraphMutation::RenameNode(RenameNodeParams {
8002 node_id: "my_field".to_string(),
8003 alias: None,
8004 name: Some("Updated Name".to_string()),
8005 description: None,
8006 realign_card: false,
8007 }),
8008 &options,
8009 )
8010 .unwrap();
8011
8012 assert_eq!(
8014 graph.find_card_by_nodegroup(&ng_id).unwrap().name.get("en"),
8015 "Original Name"
8016 );
8017
8018 apply_mutation(
8020 &mut graph,
8021 GraphMutation::RealignCardFromNode(RealignCardFromNodeParams {
8022 node_alias: "my_field".to_string(),
8023 }),
8024 &options,
8025 )
8026 .unwrap();
8027
8028 assert_eq!(
8030 graph.find_card_by_nodegroup(&ng_id).unwrap().name.get("en"),
8031 "Updated Name"
8032 );
8033 let widget = graph
8034 .cards_x_nodes_x_widgets
8035 .as_ref()
8036 .unwrap()
8037 .iter()
8038 .find(|c| c.node_id == node_id)
8039 .unwrap();
8040 assert_eq!(widget.label.get("en"), "Updated Name");
8041 }
8042
8043 #[test]
8044 fn test_realign_card_from_node_not_found() {
8045 let mut graph = create_skeleton_graph("Test", "test", false, None);
8046 let options = MutatorOptions::default();
8047
8048 let result = apply_mutation(
8049 &mut graph,
8050 GraphMutation::RealignCardFromNode(RealignCardFromNodeParams {
8051 node_alias: "nonexistent".to_string(),
8052 }),
8053 &options,
8054 );
8055
8056 assert!(matches!(result, Err(MutationError::NodeNotFound(_))));
8057 }
8058
8059 #[test]
8060 fn test_delete_widget() {
8061 let mut graph = create_skeleton_graph("Test", "test", false, None);
8063 let options = MutatorOptions::default();
8064
8065 apply_mutation(
8067 &mut graph,
8068 GraphMutation::AddNode(AddNodeParams {
8069 parent_alias: Some("test".to_string()),
8070 alias: "field1".to_string(),
8071 name: "Field 1".to_string(),
8072 cardinality: Cardinality::N,
8073 datatype: "string".to_string(),
8074 ontology_class: None,
8075 parent_property: String::new(),
8076 description: None,
8077 config: None,
8078 options: NodeOptions::default(),
8079 }),
8080 &options,
8081 )
8082 .unwrap();
8083
8084 let widget_id = graph.cards_x_nodes_x_widgets.as_ref().unwrap()[0]
8086 .id
8087 .clone();
8088 let initial_count = graph.cards_x_nodes_x_widgets.as_ref().unwrap().len();
8089
8090 apply_mutation(
8092 &mut graph,
8093 GraphMutation::DeleteWidget(DeleteWidgetParams {
8094 widget_mapping_id: widget_id.clone(),
8095 }),
8096 &options,
8097 )
8098 .unwrap();
8099
8100 let final_count = graph
8102 .cards_x_nodes_x_widgets
8103 .as_ref()
8104 .map(|c| c.len())
8105 .unwrap_or(0);
8106 assert_eq!(final_count, initial_count - 1);
8107 }
8108
8109 #[test]
8110 fn test_delete_widget_not_found() {
8111 let mut graph = create_skeleton_graph("Test", "test", false, None);
8112 let options = MutatorOptions::default();
8113
8114 let result = apply_mutation(
8115 &mut graph,
8116 GraphMutation::DeleteWidget(DeleteWidgetParams {
8117 widget_mapping_id: "nonexistent".to_string(),
8118 }),
8119 &options,
8120 );
8121
8122 assert!(matches!(result, Err(MutationError::WidgetNotFound(_))));
8123 }
8124
8125 #[test]
8126 fn test_add_function_with_uuid() {
8127 let mut graph = create_skeleton_graph("Test", "test", false, None);
8128 let options = MutatorOptions::default();
8129
8130 apply_mutation(
8131 &mut graph,
8132 GraphMutation::AddFunction(AddFunctionParams {
8133 function_id: "00b2d15a-fda0-4578-b79a-784e4138664b".to_string(),
8134 config: Some(serde_json::json!({"key": "value"})),
8135 }),
8136 &options,
8137 )
8138 .unwrap();
8139
8140 let fxgs = graph.functions_x_graphs.as_ref().unwrap();
8141 assert_eq!(fxgs.len(), 1);
8142 assert_eq!(fxgs[0].function_id, "00b2d15a-fda0-4578-b79a-784e4138664b");
8143 assert_eq!(fxgs[0].graph_id, graph.graphid);
8144 assert_eq!(fxgs[0].config["key"], "value");
8145 }
8146
8147 #[test]
8148 fn test_add_function_with_non_uuid_string() {
8149 let mut graph = create_skeleton_graph("Test", "test", false, None);
8150 let options = MutatorOptions::default();
8151
8152 apply_mutation(
8153 &mut graph,
8154 GraphMutation::AddFunction(AddFunctionParams {
8155 function_id: "com.flaxandteal.app/my-func".to_string(),
8156 config: None,
8157 }),
8158 &options,
8159 )
8160 .unwrap();
8161
8162 let fxgs = graph.functions_x_graphs.as_ref().unwrap();
8163 assert_eq!(fxgs.len(), 1);
8164 assert!(uuid::Uuid::parse_str(&fxgs[0].function_id).is_ok());
8166 assert_ne!(fxgs[0].function_id, "com.flaxandteal.app/my-func");
8168 let expected =
8170 uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, b"com.flaxandteal.app/my-func")
8171 .to_string();
8172 assert_eq!(fxgs[0].function_id, expected);
8173 }
8174
8175 #[test]
8176 fn test_add_function_duplicate() {
8177 let mut graph = create_skeleton_graph("Test", "test", false, None);
8178 let options = MutatorOptions::default();
8179
8180 apply_mutation(
8181 &mut graph,
8182 GraphMutation::AddFunction(AddFunctionParams {
8183 function_id: "00b2d15a-fda0-4578-b79a-784e4138664b".to_string(),
8184 config: None,
8185 }),
8186 &options,
8187 )
8188 .unwrap();
8189
8190 let result = apply_mutation(
8191 &mut graph,
8192 GraphMutation::AddFunction(AddFunctionParams {
8193 function_id: "00b2d15a-fda0-4578-b79a-784e4138664b".to_string(),
8194 config: None,
8195 }),
8196 &options,
8197 );
8198
8199 assert!(matches!(result, Err(MutationError::Other(_))));
8200 }
8201
8202 #[test]
8203 fn test_set_descriptor_template_with_non_default_function_allows_multi_nodegroup() {
8204 use crate::graph::StaticFunctionsXGraphs;
8205
8206 let mut graph = create_skeleton_graph("Test", "test", true, None);
8207 let options = MutatorOptions::default();
8208
8209 apply_mutation(
8211 &mut graph,
8212 GraphMutation::AddNode(AddNodeParams {
8213 parent_alias: Some("test".to_string()),
8214 alias: "name_node".to_string(),
8215 name: "Name".to_string(),
8216 datatype: "string".to_string(),
8217 cardinality: Cardinality::One,
8218 ontology_class: None,
8219 parent_property: String::new(),
8220 description: None,
8221 config: None,
8222 options: NodeOptions::default(),
8223 }),
8224 &options,
8225 )
8226 .unwrap();
8227 apply_mutation(
8228 &mut graph,
8229 GraphMutation::AddNode(AddNodeParams {
8230 parent_alias: Some("test".to_string()),
8231 alias: "desc_node".to_string(),
8232 name: "Description".to_string(),
8233 datatype: "string".to_string(),
8234 cardinality: Cardinality::One,
8235 ontology_class: None,
8236 parent_property: String::new(),
8237 description: None,
8238 config: None,
8239 options: NodeOptions::default(),
8240 }),
8241 &options,
8242 )
8243 .unwrap();
8244
8245 let name_ng = graph
8247 .nodes
8248 .iter()
8249 .find(|n| n.alias.as_deref() == Some("name_node"))
8250 .unwrap()
8251 .nodegroup_id
8252 .as_ref()
8253 .unwrap()
8254 .clone();
8255 let desc_ng = graph
8256 .nodes
8257 .iter()
8258 .find(|n| n.alias.as_deref() == Some("desc_node"))
8259 .unwrap()
8260 .nodegroup_id
8261 .as_ref()
8262 .unwrap()
8263 .clone();
8264 assert_ne!(
8265 name_ng, desc_ng,
8266 "Test setup: nodes should be in different nodegroups"
8267 );
8268
8269 let result = graph.set_descriptor_template("name", "<Name> - <Description>");
8271 assert!(
8272 result.is_err(),
8273 "Default function should reject multi-nodegroup template"
8274 );
8275 assert!(result.unwrap_err().contains("expected exactly 1"));
8276
8277 let fxg = graph.functions_x_graphs.get_or_insert_with(Vec::new);
8279 fxg.push(StaticFunctionsXGraphs {
8280 config: serde_json::json!({}),
8281 function_id: "00b2d15a-fda0-4578-b79a-784e4138664b".to_string(),
8282 graph_id: graph.graphid.clone(),
8283 id: "test-fxg-1".to_string(),
8284 });
8285
8286 let result = graph.set_descriptor_template("name", "<Name> - <Description>");
8288 assert!(
8289 result.is_ok(),
8290 "Non-default function should allow multi-nodegroup template: {:?}",
8291 result
8292 );
8293
8294 let func = graph
8296 .functions_x_graphs
8297 .as_ref()
8298 .unwrap()
8299 .iter()
8300 .find(|f| f.function_id == "00b2d15a-fda0-4578-b79a-784e4138664b")
8301 .expect("Non-default function should still exist");
8302 let dt = func.config["descriptor_types"]["name"].as_object().unwrap();
8303 assert_eq!(dt["string_template"], "<Name> - <Description>");
8304 assert_eq!(dt["nodegroup_id"], "");
8306 assert_eq!(
8308 func.config["descriptor_types"]["description"]["string_template"],
8309 ""
8310 );
8311 assert_eq!(
8312 func.config["descriptor_types"]["map_popup"]["string_template"],
8313 ""
8314 );
8315 }
8316
8317 #[test]
8318 fn test_set_descriptor_template_with_non_default_function_single_nodegroup() {
8319 use crate::graph::StaticFunctionsXGraphs;
8320
8321 let mut graph = create_skeleton_graph("Test", "test", true, None);
8322 let options = MutatorOptions::default();
8323
8324 apply_mutation(
8326 &mut graph,
8327 GraphMutation::AddNode(AddNodeParams {
8328 parent_alias: Some("test".to_string()),
8329 alias: "name_node".to_string(),
8330 name: "Name".to_string(),
8331 datatype: "string".to_string(),
8332 cardinality: Cardinality::One,
8333 ontology_class: None,
8334 parent_property: String::new(),
8335 description: None,
8336 config: None,
8337 options: NodeOptions::default(),
8338 }),
8339 &options,
8340 )
8341 .unwrap();
8342
8343 let fxg = graph.functions_x_graphs.get_or_insert_with(Vec::new);
8345 fxg.push(StaticFunctionsXGraphs {
8346 config: serde_json::json!({}),
8347 function_id: "00b2d15a-fda0-4578-b79a-784e4138664b".to_string(),
8348 graph_id: graph.graphid.clone(),
8349 id: "test-fxg-1".to_string(),
8350 });
8351
8352 let result = graph.set_descriptor_template("name", "<Name>");
8354 assert!(
8355 result.is_ok(),
8356 "Non-default function should allow single-nodegroup template: {:?}",
8357 result
8358 );
8359
8360 let func = graph
8361 .functions_x_graphs
8362 .as_ref()
8363 .unwrap()
8364 .iter()
8365 .find(|f| f.function_id == "00b2d15a-fda0-4578-b79a-784e4138664b")
8366 .expect("Non-default function should still exist");
8367 let dt = func.config["descriptor_types"]["name"].as_object().unwrap();
8368 assert_eq!(dt["string_template"], "<Name>");
8369 }
8370
8371 #[test]
8372 fn test_delete_function() {
8373 use crate::graph::StaticFunctionsXGraphs;
8374
8375 let mut graph = create_skeleton_graph("Test", "test", false, None);
8377 let options = MutatorOptions::default();
8378
8379 graph.functions_x_graphs = Some(vec![StaticFunctionsXGraphs {
8381 id: "func-mapping-1".to_string(),
8382 function_id: "60000000-0000-0000-0000-000000000001".to_string(),
8383 graph_id: graph.graphid.clone(),
8384 config: serde_json::Value::Object(serde_json::Map::new()),
8385 }]);
8386
8387 apply_mutation(
8389 &mut graph,
8390 GraphMutation::DeleteFunction(DeleteFunctionParams {
8391 function_mapping_id: "func-mapping-1".to_string(),
8392 }),
8393 &options,
8394 )
8395 .unwrap();
8396
8397 assert!(graph
8399 .functions_x_graphs
8400 .as_ref()
8401 .map(|f| f.is_empty())
8402 .unwrap_or(true));
8403 }
8404
8405 #[test]
8406 fn test_delete_function_not_found() {
8407 let mut graph = create_skeleton_graph("Test", "test", false, None);
8408 let options = MutatorOptions::default();
8409
8410 let result = apply_mutation(
8411 &mut graph,
8412 GraphMutation::DeleteFunction(DeleteFunctionParams {
8413 function_mapping_id: "nonexistent".to_string(),
8414 }),
8415 &options,
8416 );
8417
8418 assert!(matches!(result, Err(MutationError::FunctionNotFound(_))));
8419 }
8420
8421 #[test]
8422 fn test_delete_node() {
8423 let mut graph = create_skeleton_graph("Test", "test", false, None);
8424 let options = MutatorOptions::default();
8425
8426 apply_mutation(
8428 &mut graph,
8429 GraphMutation::AddNode(AddNodeParams {
8430 parent_alias: Some("test".to_string()),
8431 alias: "field1".to_string(),
8432 name: "Field 1".to_string(),
8433 cardinality: Cardinality::N,
8434 datatype: "string".to_string(),
8435 ontology_class: None,
8436 parent_property: String::new(),
8437 description: None,
8438 config: None,
8439 options: NodeOptions::default(),
8440 }),
8441 &options,
8442 )
8443 .unwrap();
8444
8445 let initial_node_count = graph.nodes.len();
8446 let initial_edge_count = graph.edges.len();
8447
8448 apply_mutation(
8450 &mut graph,
8451 GraphMutation::DeleteNode(DeleteNodeParams {
8452 node_id: "field1".to_string(),
8453 }),
8454 &options,
8455 )
8456 .unwrap();
8457
8458 assert_eq!(graph.nodes.len(), initial_node_count - 1);
8460 assert!(graph.find_node_by_alias("field1").is_none());
8461
8462 assert!(graph.edges.len() < initial_edge_count);
8464
8465 let has_widget_for_node = graph
8467 .cards_x_nodes_x_widgets
8468 .as_ref()
8469 .map(|c| c.iter().any(|w| w.node_id.contains("field1")))
8470 .unwrap_or(false);
8471 assert!(!has_widget_for_node);
8472 }
8473
8474 #[test]
8475 fn test_delete_node_not_found() {
8476 let mut graph = create_skeleton_graph("Test", "test", false, None);
8477 let options = MutatorOptions::default();
8478
8479 let result = apply_mutation(
8480 &mut graph,
8481 GraphMutation::DeleteNode(DeleteNodeParams {
8482 node_id: "nonexistent".to_string(),
8483 }),
8484 &options,
8485 );
8486
8487 assert!(matches!(result, Err(MutationError::NodeNotFound(_))));
8488 }
8489
8490 #[test]
8491 fn test_delete_node_cannot_delete_root() {
8492 let mut graph = create_skeleton_graph("Test", "test", false, None);
8493 let options = MutatorOptions::default();
8494
8495 let result = apply_mutation(
8497 &mut graph,
8498 GraphMutation::DeleteNode(DeleteNodeParams {
8499 node_id: "test".to_string(),
8500 }),
8501 &options,
8502 );
8503
8504 assert!(matches!(
8505 result,
8506 Err(MutationError::CannotDeleteRootNode(_))
8507 ));
8508 }
8509
8510 #[test]
8511 fn test_delete_nodegroup_cascade() {
8512 let mut graph = create_skeleton_graph("Test", "test", false, None);
8513 let options = MutatorOptions::default();
8514
8515 apply_mutation(
8517 &mut graph,
8518 GraphMutation::AddNode(AddNodeParams {
8519 parent_alias: Some("test".to_string()),
8520 alias: "parent_field".to_string(),
8521 name: "Parent Field".to_string(),
8522 cardinality: Cardinality::N,
8523 datatype: "semantic".to_string(),
8524 ontology_class: None,
8525 parent_property: String::new(),
8526 description: None,
8527 config: None,
8528 options: NodeOptions::default(),
8529 }),
8530 &options,
8531 )
8532 .unwrap();
8533
8534 apply_mutation(
8536 &mut graph,
8537 GraphMutation::AddNode(AddNodeParams {
8538 parent_alias: Some("parent_field".to_string()),
8539 alias: "child_field".to_string(),
8540 name: "Child Field".to_string(),
8541 cardinality: Cardinality::One,
8542 datatype: "string".to_string(),
8543 ontology_class: None,
8544 parent_property: String::new(),
8545 description: None,
8546 config: None,
8547 options: NodeOptions::default(),
8548 }),
8549 &options,
8550 )
8551 .unwrap();
8552
8553 let parent_node = graph.find_node_by_alias("parent_field").unwrap();
8555 let nodegroup_id = parent_node.nodegroup_id.clone().unwrap();
8556
8557 let initial_node_count = graph.nodes.len();
8558 let initial_nodegroup_count = graph.nodegroups.len();
8559
8560 apply_mutation(
8562 &mut graph,
8563 GraphMutation::DeleteNodegroup(DeleteNodegroupParams {
8564 nodegroup_id: nodegroup_id.clone(),
8565 }),
8566 &options,
8567 )
8568 .unwrap();
8569
8570 assert!(graph
8572 .nodegroups
8573 .iter()
8574 .all(|ng| ng.nodegroupid != nodegroup_id));
8575
8576 assert!(graph.find_node_by_alias("parent_field").is_none());
8578
8579 assert!(graph.find_node_by_alias("child_field").is_none());
8581
8582 assert!(graph.nodes.len() < initial_node_count);
8584 assert!(graph.nodegroups.len() < initial_nodegroup_count);
8585 }
8586
8587 #[test]
8588 fn test_delete_nodegroup_not_found() {
8589 let mut graph = create_skeleton_graph("Test", "test", false, None);
8590 let options = MutatorOptions::default();
8591
8592 let result = apply_mutation(
8593 &mut graph,
8594 GraphMutation::DeleteNodegroup(DeleteNodegroupParams {
8595 nodegroup_id: "nonexistent".to_string(),
8596 }),
8597 &options,
8598 );
8599
8600 assert!(matches!(result, Err(MutationError::NodegroupNotFound(_))));
8601 }
8602
8603 #[test]
8604 fn test_delete_node_via_instruction() {
8605 let mut graph = create_skeleton_graph("Test", "test", false, None);
8606 let options = MutatorOptions::default();
8607
8608 apply_mutation(
8610 &mut graph,
8611 GraphMutation::AddNode(AddNodeParams {
8612 parent_alias: Some("test".to_string()),
8613 alias: "my_field".to_string(),
8614 name: "My Field".to_string(),
8615 cardinality: Cardinality::N,
8616 datatype: "string".to_string(),
8617 ontology_class: None,
8618 parent_property: String::new(),
8619 description: None,
8620 config: None,
8621 options: NodeOptions::default(),
8622 }),
8623 &options,
8624 )
8625 .unwrap();
8626
8627 let instruction = GraphInstruction::new("delete_node", "my_field", "");
8629 let mutation = instruction.to_mutation().unwrap();
8630
8631 apply_mutation(&mut graph, mutation, &options).unwrap();
8632
8633 assert!(graph.find_node_by_alias("my_field").is_none());
8635 }
8636
8637 #[test]
8642 fn test_update_node() {
8643 let mut graph = create_skeleton_graph("Test", "test", false, None);
8644 let options = MutatorOptions::default();
8645
8646 apply_mutation(
8648 &mut graph,
8649 GraphMutation::AddNode(AddNodeParams {
8650 parent_alias: Some("test".to_string()),
8651 alias: "field1".to_string(),
8652 name: "Original Name".to_string(),
8653 cardinality: Cardinality::N,
8654 datatype: "string".to_string(),
8655 ontology_class: None,
8656 parent_property: String::new(),
8657 description: None,
8658 config: None,
8659 options: NodeOptions::default(),
8660 }),
8661 &options,
8662 )
8663 .unwrap();
8664
8665 apply_mutation(
8667 &mut graph,
8668 GraphMutation::UpdateNode(UpdateNodeParams {
8669 node_id: "field1".to_string(),
8670 name: Some("Updated Name".to_string()),
8671 ontology_class: Some(vec!["http://example.org/Class".to_string()]),
8672 parent_property: None,
8673 description: Some("A description".to_string()),
8674 config: None,
8675 options: UpdateNodeOptions {
8676 isrequired: Some(true),
8677 ..UpdateNodeOptions::default()
8678 },
8679 }),
8680 &options,
8681 )
8682 .unwrap();
8683
8684 let node = graph.find_node_by_alias("field1").unwrap();
8686 assert_eq!(node.name, "Updated Name");
8687 assert_eq!(
8688 node.ontologyclass,
8689 Some(vec!["http://example.org/Class".to_string()])
8690 );
8691 assert!(node.description.is_some());
8692 assert!(node.isrequired);
8693 assert_eq!(node.datatype, "string");
8695 }
8696
8697 #[test]
8698 fn test_update_node_not_found() {
8699 let mut graph = create_skeleton_graph("Test", "test", false, None);
8700 let options = MutatorOptions::default();
8701
8702 let result = apply_mutation(
8703 &mut graph,
8704 GraphMutation::UpdateNode(UpdateNodeParams {
8705 node_id: "nonexistent".to_string(),
8706 name: Some("New Name".to_string()),
8707 ontology_class: None,
8708 parent_property: None,
8709 description: None,
8710 config: None,
8711 options: UpdateNodeOptions::default(),
8712 }),
8713 &options,
8714 );
8715
8716 assert!(matches!(result, Err(MutationError::NodeNotFound(_))));
8717 }
8718
8719 #[test]
8720 fn test_change_node_type() {
8721 let mut graph = create_skeleton_graph("Test", "test", false, None);
8722 let options = MutatorOptions {
8724 autocreate_card: true,
8725 autocreate_widget: false,
8726 ontology_validator: None,
8727 skip_publication: false,
8728 };
8729
8730 apply_mutation(
8732 &mut graph,
8733 GraphMutation::AddNode(AddNodeParams {
8734 parent_alias: Some("test".to_string()),
8735 alias: "field1".to_string(),
8736 name: "Field 1".to_string(),
8737 cardinality: Cardinality::N,
8738 datatype: "semantic".to_string(),
8739 ontology_class: None,
8740 parent_property: String::new(),
8741 description: None,
8742 config: None,
8743 options: NodeOptions::default(),
8744 }),
8745 &options,
8746 )
8747 .unwrap();
8748
8749 apply_mutation(
8751 &mut graph,
8752 GraphMutation::ChangeNodeType(ChangeNodeTypeParams {
8753 node_id: "field1".to_string(),
8754 datatype: "string".to_string(),
8755 name: Some("Field 1 String".to_string()),
8756 ontology_class: None,
8757 parent_property: None,
8758 description: None,
8759 config: None,
8760 options: UpdateNodeOptions::default(),
8761 }),
8762 &options,
8763 )
8764 .unwrap();
8765
8766 let node = graph.find_node_by_alias("field1").unwrap();
8768 assert_eq!(node.datatype, "string");
8769 assert_eq!(node.name, "Field 1 String");
8770 }
8771
8772 #[test]
8773 fn test_change_node_type_with_widgets_error() {
8774 let mut graph = create_skeleton_graph("Test", "test", false, None);
8775 let options = MutatorOptions::default(); apply_mutation(
8779 &mut graph,
8780 GraphMutation::AddNode(AddNodeParams {
8781 parent_alias: Some("test".to_string()),
8782 alias: "field1".to_string(),
8783 name: "Field 1".to_string(),
8784 cardinality: Cardinality::N,
8785 datatype: "string".to_string(),
8786 ontology_class: None,
8787 parent_property: String::new(),
8788 description: None,
8789 config: None,
8790 options: NodeOptions::default(),
8791 }),
8792 &options,
8793 )
8794 .unwrap();
8795
8796 let node = graph.find_node_by_alias("field1").unwrap();
8798 let has_widget = graph
8799 .cards_x_nodes_x_widgets
8800 .as_ref()
8801 .map(|cxnxws| cxnxws.iter().any(|c| c.node_id == node.nodeid))
8802 .unwrap_or(false);
8803 assert!(has_widget, "Widget should exist");
8804
8805 let result = apply_mutation(
8807 &mut graph,
8808 GraphMutation::ChangeNodeType(ChangeNodeTypeParams {
8809 node_id: "field1".to_string(),
8810 datatype: "number".to_string(),
8811 name: None,
8812 ontology_class: None,
8813 parent_property: None,
8814 description: None,
8815 config: None,
8816 options: UpdateNodeOptions::default(),
8817 }),
8818 &options,
8819 );
8820
8821 assert!(matches!(
8822 result,
8823 Err(MutationError::NodeHasDependentWidgets(_))
8824 ));
8825 }
8826
8827 #[test]
8828 fn test_rename_node() {
8829 let mut graph = create_skeleton_graph("Test", "test", false, None);
8830 let options = MutatorOptions::default();
8831
8832 apply_mutation(
8834 &mut graph,
8835 GraphMutation::AddNode(AddNodeParams {
8836 parent_alias: Some("test".to_string()),
8837 alias: "old_alias".to_string(),
8838 name: "Old Name".to_string(),
8839 cardinality: Cardinality::N,
8840 datatype: "string".to_string(),
8841 ontology_class: None,
8842 parent_property: String::new(),
8843 description: None,
8844 config: None,
8845 options: NodeOptions::default(),
8846 }),
8847 &options,
8848 )
8849 .unwrap();
8850
8851 apply_mutation(
8853 &mut graph,
8854 GraphMutation::RenameNode(RenameNodeParams {
8855 node_id: "old_alias".to_string(),
8856 alias: Some("new_alias".to_string()),
8857 name: Some("New Name".to_string()),
8858 description: Some("New description".to_string()),
8859 realign_card: true,
8860 }),
8861 &options,
8862 )
8863 .unwrap();
8864
8865 assert!(graph.find_node_by_alias("old_alias").is_none());
8867
8868 let node = graph.find_node_by_alias("new_alias").unwrap();
8870 assert_eq!(node.name, "New Name");
8871 assert!(node.description.is_some());
8872 }
8873
8874 #[test]
8875 fn test_rename_node_alias_conflict() {
8876 let mut graph = create_skeleton_graph("Test", "test", false, None);
8877 let options = MutatorOptions::default();
8878
8879 apply_mutation(
8881 &mut graph,
8882 GraphMutation::AddNode(AddNodeParams {
8883 parent_alias: Some("test".to_string()),
8884 alias: "field1".to_string(),
8885 name: "Field 1".to_string(),
8886 cardinality: Cardinality::N,
8887 datatype: "string".to_string(),
8888 ontology_class: None,
8889 parent_property: String::new(),
8890 description: None,
8891 config: None,
8892 options: NodeOptions::default(),
8893 }),
8894 &options,
8895 )
8896 .unwrap();
8897
8898 apply_mutation(
8899 &mut graph,
8900 GraphMutation::AddNode(AddNodeParams {
8901 parent_alias: Some("test".to_string()),
8902 alias: "field2".to_string(),
8903 name: "Field 2".to_string(),
8904 cardinality: Cardinality::N,
8905 datatype: "string".to_string(),
8906 ontology_class: None,
8907 parent_property: String::new(),
8908 description: None,
8909 config: None,
8910 options: NodeOptions::default(),
8911 }),
8912 &options,
8913 )
8914 .unwrap();
8915
8916 let result = apply_mutation(
8918 &mut graph,
8919 GraphMutation::RenameNode(RenameNodeParams {
8920 node_id: "field1".to_string(),
8921 alias: Some("field2".to_string()),
8922 name: None,
8923 description: None,
8924 realign_card: true,
8925 }),
8926 &options,
8927 );
8928
8929 assert!(matches!(result, Err(MutationError::AliasAlreadyExists(_))));
8930 }
8931
8932 #[test]
8933 fn test_update_node_via_instruction() {
8934 let mut graph = create_skeleton_graph("Test", "test", false, None);
8935 let options = MutatorOptions::default();
8936
8937 apply_mutation(
8939 &mut graph,
8940 GraphMutation::AddNode(AddNodeParams {
8941 parent_alias: Some("test".to_string()),
8942 alias: "my_field".to_string(),
8943 name: "My Field".to_string(),
8944 cardinality: Cardinality::N,
8945 datatype: "string".to_string(),
8946 ontology_class: None,
8947 parent_property: String::new(),
8948 description: None,
8949 config: None,
8950 options: NodeOptions::default(),
8951 }),
8952 &options,
8953 )
8954 .unwrap();
8955
8956 let instruction = GraphInstruction::new("update_node", "my_field", "")
8958 .with_str("name", "Updated Field Name")
8959 .with_param("isrequired", serde_json::Value::Bool(true));
8960 let mutation = instruction.to_mutation().unwrap();
8961
8962 apply_mutation(&mut graph, mutation, &options).unwrap();
8963
8964 let node = graph.find_node_by_alias("my_field").unwrap();
8966 assert_eq!(node.name, "Updated Field Name");
8967 assert!(node.isrequired);
8968 }
8969
8970 #[test]
8971 fn test_rename_node_via_instruction() {
8972 let mut graph = create_skeleton_graph("Test", "test", false, None);
8973 let options = MutatorOptions::default();
8974
8975 apply_mutation(
8977 &mut graph,
8978 GraphMutation::AddNode(AddNodeParams {
8979 parent_alias: Some("test".to_string()),
8980 alias: "old_name".to_string(),
8981 name: "Old Name".to_string(),
8982 cardinality: Cardinality::N,
8983 datatype: "string".to_string(),
8984 ontology_class: None,
8985 parent_property: String::new(),
8986 description: None,
8987 config: None,
8988 options: NodeOptions::default(),
8989 }),
8990 &options,
8991 )
8992 .unwrap();
8993
8994 let instruction = GraphInstruction::new("rename_node", "old_name", "new_name")
8996 .with_str("name", "New Display Name");
8997 let mutation = instruction.to_mutation().unwrap();
8998
8999 apply_mutation(&mut graph, mutation, &options).unwrap();
9000
9001 assert!(graph.find_node_by_alias("old_name").is_none());
9003 let node = graph.find_node_by_alias("new_name").unwrap();
9004 assert_eq!(node.name, "New Display Name");
9005 }
9006
9007 struct TestPrefixHandler {
9013 prefix: String,
9014 }
9015
9016 impl ExtensionMutationHandler for TestPrefixHandler {
9017 fn apply(
9018 &self,
9019 graph: &mut StaticGraph,
9020 params: &serde_json::Value,
9021 _options: &MutatorOptions,
9022 ) -> Result<(), MutationError> {
9023 let suffix = params.get("suffix").and_then(|v| v.as_str()).unwrap_or("");
9024
9025 let root_id = graph.get_root().nodeid.clone();
9027 let root_name = graph.get_root().name.clone();
9028 if let Some(node) = graph.nodes.iter_mut().find(|n| n.nodeid == root_id) {
9029 node.name = format!("{}{}{}", self.prefix, root_name, suffix);
9030 }
9031 Ok(())
9032 }
9033
9034 fn conformance(&self) -> MutationConformance {
9035 MutationConformance::AlwaysConformant
9036 }
9037
9038 fn description(&self) -> &str {
9039 "Test handler that adds prefix/suffix to root name"
9040 }
9041 }
9042
9043 #[test]
9044 fn test_extension_mutation_with_registry() {
9045 let graph = create_test_graph();
9046 let options = MutatorOptions::default();
9047
9048 let mut registry = ExtensionMutationRegistry::new();
9050 registry.register(
9051 "test.prefix_name",
9052 std::sync::Arc::new(TestPrefixHandler {
9053 prefix: "[PREFIX] ".to_string(),
9054 }),
9055 );
9056
9057 let mutation = GraphMutation::Extension(ExtensionMutationParams {
9059 name: "test.prefix_name".to_string(),
9060 params: serde_json::json!({"suffix": " [SUFFIX]"}),
9061 conformance: MutationConformance::AlwaysConformant,
9062 });
9063
9064 let result =
9066 apply_mutations_with_extensions(&graph, vec![mutation], options, Some(®istry));
9067
9068 assert!(result.is_ok());
9069 let mutated = result.unwrap();
9070 let root_node = mutated.nodes.iter().find(|n| n.istopnode).unwrap();
9072 assert_eq!(root_node.name, "[PREFIX] Root [SUFFIX]");
9073 }
9074
9075 #[test]
9076 fn test_extension_mutation_without_registry() {
9077 let graph = create_test_graph();
9078 let options = MutatorOptions::default();
9079
9080 let mutation = GraphMutation::Extension(ExtensionMutationParams {
9082 name: "test.some_mutation".to_string(),
9083 params: serde_json::json!({}),
9084 conformance: MutationConformance::AlwaysConformant,
9085 });
9086
9087 let result = apply_mutations(&graph, vec![mutation], options);
9089
9090 assert!(result.is_err());
9091 assert!(result.unwrap_err().contains("no registry provided"));
9092 }
9093
9094 #[test]
9095 fn test_extension_mutation_not_found() {
9096 let graph = create_test_graph();
9097 let options = MutatorOptions::default();
9098
9099 let registry = ExtensionMutationRegistry::new();
9101
9102 let mutation = GraphMutation::Extension(ExtensionMutationParams {
9104 name: "test.nonexistent".to_string(),
9105 params: serde_json::json!({}),
9106 conformance: MutationConformance::AlwaysConformant,
9107 });
9108
9109 let result =
9111 apply_mutations_with_extensions(&graph, vec![mutation], options, Some(®istry));
9112
9113 assert!(result.is_err());
9114 assert!(result.unwrap_err().contains("not found"));
9115 }
9116
9117 #[test]
9118 fn test_extension_registry_operations() {
9119 let mut registry = ExtensionMutationRegistry::new();
9120
9121 assert!(!registry.has("test.handler"));
9122 assert!(registry.list().is_empty());
9123
9124 registry.register(
9125 "test.handler",
9126 std::sync::Arc::new(TestPrefixHandler {
9127 prefix: "x".to_string(),
9128 }),
9129 );
9130
9131 assert!(registry.has("test.handler"));
9132 assert!(!registry.has("test.other"));
9133 assert_eq!(registry.list().len(), 1);
9134 assert!(registry.get("test.handler").is_some());
9135 }
9136
9137 #[test]
9138 fn test_extension_mutation_conformance() {
9139 let mutation = GraphMutation::Extension(ExtensionMutationParams {
9141 name: "test.mutation".to_string(),
9142 params: serde_json::json!({}),
9143 conformance: MutationConformance::BranchConformant,
9144 });
9145
9146 assert_eq!(
9147 mutation.conformance(),
9148 MutationConformance::BranchConformant
9149 );
9150
9151 let mutation2 = GraphMutation::Extension(ExtensionMutationParams {
9152 name: "test.mutation".to_string(),
9153 params: serde_json::json!({}),
9154 conformance: MutationConformance::ModelConformant,
9155 });
9156
9157 assert_eq!(
9158 mutation2.conformance(),
9159 MutationConformance::ModelConformant
9160 );
9161 }
9162
9163 #[test]
9164 fn test_extension_mutation_serialization() {
9165 let mutation = GraphMutation::Extension(ExtensionMutationParams {
9166 name: "clm.reference_change_collection".to_string(),
9167 params: serde_json::json!({
9168 "node_id": "my_node",
9169 "collection_id": "new-collection"
9170 }),
9171 conformance: MutationConformance::AlwaysConformant,
9172 });
9173
9174 let json = serde_json::to_string(&mutation).unwrap();
9176 assert!(json.contains("clm.reference_change_collection"));
9177 assert!(json.contains("my_node"));
9178
9179 let parsed: GraphMutation = serde_json::from_str(&json).unwrap();
9181 if let GraphMutation::Extension(params) = parsed {
9182 assert_eq!(params.name, "clm.reference_change_collection");
9183 assert_eq!(params.params["node_id"], "my_node");
9184 } else {
9185 panic!("Expected Extension mutation");
9186 }
9187 }
9188
9189 #[test]
9190 fn test_extension_mutation_from_json() {
9191 let graph = create_test_graph();
9192
9193 let mut registry = ExtensionMutationRegistry::new();
9194 registry.register(
9195 "test.prefix_name",
9196 std::sync::Arc::new(TestPrefixHandler {
9197 prefix: "[TEST] ".to_string(),
9198 }),
9199 );
9200
9201 let mutations_json = r#"{
9202 "mutations": [{
9203 "Extension": {
9204 "name": "test.prefix_name",
9205 "params": {"suffix": "!"},
9206 "conformance": "AlwaysConformant"
9207 }
9208 }],
9209 "options": {}
9210 }"#;
9211
9212 let result =
9213 apply_mutations_from_json_with_extensions(&graph, mutations_json, Some(®istry));
9214
9215 assert!(result.is_ok());
9216 let mutated = result.unwrap();
9217 let root_node = mutated.nodes.iter().find(|n| n.istopnode).unwrap();
9219 assert_eq!(root_node.name, "[TEST] Root!");
9220 }
9221
9222 #[test]
9227 fn test_rename_graph() {
9228 let mut graph = create_skeleton_graph("Test Graph", "test", false, None);
9229 let options = MutatorOptions::default();
9230
9231 assert_eq!(graph.name.get("en"), "Test Graph");
9233 assert!(graph.description.is_none());
9234 assert!(graph.subtitle.is_none());
9235 assert!(graph.author.is_none());
9236
9237 let mut name_map = HashMap::new();
9239 name_map.insert("en".to_string(), "New Name".to_string());
9240 name_map.insert("es".to_string(), "Nuevo Nombre".to_string());
9241
9242 let mut desc_map = HashMap::new();
9243 desc_map.insert("en".to_string(), "A description".to_string());
9244
9245 let mut subtitle_map = HashMap::new();
9246 subtitle_map.insert("en".to_string(), "A subtitle".to_string());
9247
9248 apply_mutation(
9249 &mut graph,
9250 GraphMutation::RenameGraph(RenameGraphParams {
9251 name: Some(name_map),
9252 description: Some(desc_map),
9253 subtitle: Some(subtitle_map),
9254 author: Some("Test Author".to_string()),
9255 }),
9256 &options,
9257 )
9258 .unwrap();
9259
9260 assert_eq!(graph.name.get("en"), "New Name");
9262 assert_eq!(graph.name.translations.get("es").unwrap(), "Nuevo Nombre");
9263 assert!(graph.description.is_some());
9264 assert_eq!(
9265 graph.description.as_ref().unwrap().get("en"),
9266 "A description"
9267 );
9268 assert!(graph.subtitle.is_some());
9269 assert_eq!(graph.subtitle.as_ref().unwrap().get("en"), "A subtitle");
9270 assert_eq!(graph.author, Some("Test Author".to_string()));
9271
9272 assert_eq!(graph.root.name, "New Name");
9274 let root_in_nodes = graph.nodes.iter().find(|n| n.istopnode).unwrap();
9275 assert_eq!(root_in_nodes.name, "New Name");
9276
9277 assert_eq!(graph.slug, Some("new_name".to_string()));
9279 assert_eq!(graph.root.alias, Some("new_name".to_string()));
9280 assert_eq!(root_in_nodes.alias, Some("new_name".to_string()));
9281 }
9282
9283 #[test]
9284 fn test_rename_graph_partial() {
9285 let mut graph = create_skeleton_graph("Original Name", "test", false, None);
9286 let options = MutatorOptions::default();
9287
9288 let mut desc_map = HashMap::new();
9290 desc_map.insert("en".to_string(), "New description".to_string());
9291
9292 apply_mutation(
9293 &mut graph,
9294 GraphMutation::RenameGraph(RenameGraphParams {
9295 name: None,
9296 description: Some(desc_map),
9297 subtitle: None,
9298 author: None,
9299 }),
9300 &options,
9301 )
9302 .unwrap();
9303
9304 assert_eq!(graph.name.get("en"), "Original Name");
9306 assert!(graph.description.is_some());
9308 assert_eq!(
9309 graph.description.as_ref().unwrap().get("en"),
9310 "New description"
9311 );
9312 }
9313
9314 #[test]
9315 fn test_rename_graph_via_instruction() {
9316 let mut graph = create_skeleton_graph("Test Graph", "test", false, None);
9317 let options = MutatorOptions::default();
9318
9319 let instruction = GraphInstruction::new("rename_graph", "test", "New Graph Name")
9321 .with_str("author", "Instruction Author");
9322
9323 assert_eq!(
9325 instruction.conformance(),
9326 MutationConformance::AlwaysConformant
9327 );
9328
9329 let mutation = instruction.to_mutation().unwrap();
9330 apply_mutation(&mut graph, mutation, &options).unwrap();
9331
9332 assert_eq!(graph.name.get("en"), "New Graph Name");
9334 assert_eq!(graph.author, Some("Instruction Author".to_string()));
9335 }
9336
9337 #[test]
9338 fn test_rename_graph_via_instruction_multilingual() {
9339 let mut graph = create_skeleton_graph("Test Graph", "test", false, None);
9340 let options = MutatorOptions::default();
9341
9342 let mut name_obj = serde_json::Map::new();
9344 name_obj.insert(
9345 "en".to_string(),
9346 serde_json::Value::String("English Name".to_string()),
9347 );
9348 name_obj.insert(
9349 "de".to_string(),
9350 serde_json::Value::String("Deutscher Name".to_string()),
9351 );
9352
9353 let mut desc_obj = serde_json::Map::new();
9354 desc_obj.insert(
9355 "en".to_string(),
9356 serde_json::Value::String("English description".to_string()),
9357 );
9358
9359 let instruction = GraphInstruction::new("rename_graph", "test", "")
9360 .with_param("name", serde_json::Value::Object(name_obj))
9361 .with_param("description", serde_json::Value::Object(desc_obj));
9362
9363 let mutation = instruction.to_mutation().unwrap();
9364 apply_mutation(&mut graph, mutation, &options).unwrap();
9365
9366 assert_eq!(graph.name.get("en"), "English Name");
9368 assert_eq!(graph.name.translations.get("de").unwrap(), "Deutscher Name");
9369 assert!(graph.description.is_some());
9371 assert_eq!(
9372 graph.description.as_ref().unwrap().get("en"),
9373 "English description"
9374 );
9375 }
9376}