diff --git a/lib/dal/src/workspace_snapshot.rs b/lib/dal/src/workspace_snapshot.rs index a9b3946d91..0197158532 100644 --- a/lib/dal/src/workspace_snapshot.rs +++ b/lib/dal/src/workspace_snapshot.rs @@ -49,12 +49,15 @@ use thiserror::Error; use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::task::JoinError; +use self::graph::ConflictsAndUpdates; +use self::node_weight::{NodeWeightDiscriminants, OrderingNodeWeight}; use crate::action::{Action, ActionError}; use crate::attribute::prototype::argument::{ AttributePrototypeArgument, AttributePrototypeArgumentError, AttributePrototypeArgumentId, }; use crate::change_set::{ChangeSetError, ChangeSetId}; use crate::slow_rt::{self, SlowRuntimeError}; +use crate::workspace_snapshot::content_address::ContentAddressDiscriminants; use crate::workspace_snapshot::edge_weight::{ EdgeWeight, EdgeWeightError, EdgeWeightKind, EdgeWeightKindDiscriminants, }; @@ -63,15 +66,15 @@ use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKi use crate::workspace_snapshot::node_weight::NodeWeight; use crate::workspace_snapshot::update::Update; use crate::workspace_snapshot::vector_clock::VectorClockId; -use crate::{pk, ChangeSet, Component, ComponentError, ComponentId, Workspace, WorkspaceError}; +use crate::{ + pk, AttributeValueId, ChangeSet, Component, ComponentError, ComponentId, Workspace, + WorkspaceError, +}; use crate::{ workspace_snapshot::{graph::WorkspaceSnapshotGraphError, node_weight::NodeWeightError}, DalContext, TransactionsError, WorkspaceSnapshotGraphV1, }; -use self::graph::ConflictsAndUpdates; -use self::node_weight::{NodeWeightDiscriminants, OrderingNodeWeight}; - pk!(NodeId); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct NodeInformation { @@ -2121,4 +2124,78 @@ impl WorkspaceSnapshot { Ok(()) } + + /// If this node is associated to a single av, return it + pub async fn associated_attribute_value_id( + &self, + node_weight: NodeWeight, + ) -> WorkspaceSnapshotResult> { + let mut this_node_weight = node_weight; + while let Some(edge_kind) = match &this_node_weight { + NodeWeight::AttributeValue(av) => return Ok(Some(av.id().into())), + NodeWeight::AttributePrototypeArgument(_) => { + Some(EdgeWeightKindDiscriminants::PrototypeArgument) + } + NodeWeight::Ordering(_) => Some(EdgeWeightKindDiscriminants::Ordering), + + NodeWeight::Content(content) => match content.content_address_discriminants() { + ContentAddressDiscriminants::AttributePrototype => { + Some(EdgeWeightKindDiscriminants::Prototype) + } + ContentAddressDiscriminants::StaticArgumentValue => { + Some(EdgeWeightKindDiscriminants::PrototypeArgumentValue) + } + ContentAddressDiscriminants::ValidationOutput => { + Some(EdgeWeightKindDiscriminants::ValidationOutput) + } + + ContentAddressDiscriminants::ActionPrototype + | ContentAddressDiscriminants::Component + | ContentAddressDiscriminants::DeprecatedAction + | ContentAddressDiscriminants::DeprecatedActionBatch + | ContentAddressDiscriminants::DeprecatedActionRunner + | ContentAddressDiscriminants::Func + | ContentAddressDiscriminants::FuncArg + | ContentAddressDiscriminants::InputSocket + | ContentAddressDiscriminants::JsonValue + | ContentAddressDiscriminants::Module + | ContentAddressDiscriminants::OutputSocket + | ContentAddressDiscriminants::Prop + | ContentAddressDiscriminants::Root + | ContentAddressDiscriminants::Schema + | ContentAddressDiscriminants::SchemaVariant + | ContentAddressDiscriminants::Secret + | ContentAddressDiscriminants::ValidationPrototype => None, + }, + + NodeWeight::Action(_) + | NodeWeight::ActionPrototype(_) + | NodeWeight::Category(_) + | NodeWeight::Component(_) + | NodeWeight::DependentValueRoot(_) + | NodeWeight::Func(_) + | NodeWeight::FuncArgument(_) + | NodeWeight::Prop(_) + | NodeWeight::Secret(_) => None, + } { + let next_node_idxs = self + .incoming_sources_for_edge_weight_kind(this_node_weight.id(), edge_kind) + .await?; + + this_node_weight = match next_node_idxs.first() { + Some(&next_node_idx) if next_node_idxs.len() == 1 => { + self.get_node_weight(next_node_idx).await? + } + _ => { + return Err(WorkspaceSnapshotError::UnexpectedNumberOfIncomingEdges( + edge_kind, + NodeWeightDiscriminants::from(&this_node_weight), + this_node_weight.id(), + )) + } + }; + } + + Ok(None) + } } diff --git a/lib/dal/src/workspace_snapshot/conflict.rs b/lib/dal/src/workspace_snapshot/conflict.rs index fc5d873145..fd263c81c3 100644 --- a/lib/dal/src/workspace_snapshot/conflict.rs +++ b/lib/dal/src/workspace_snapshot/conflict.rs @@ -16,7 +16,10 @@ pub enum Conflict { destination: NodeInformation, edge_kind: EdgeWeightKindDiscriminants, }, - ModifyRemovedItem(NodeInformation), + ModifyRemovedItem { + container: NodeInformation, + modified_item: NodeInformation, + }, NodeContent { onto: NodeInformation, to_rebase: NodeInformation, diff --git a/lib/dal/src/workspace_snapshot/graph/detect_conflicts_and_updates.rs b/lib/dal/src/workspace_snapshot/graph/detect_conflicts_and_updates.rs index 00c50ebefd..7e82378d13 100644 --- a/lib/dal/src/workspace_snapshot/graph/detect_conflicts_and_updates.rs +++ b/lib/dal/src/workspace_snapshot/graph/detect_conflicts_and_updates.rs @@ -447,7 +447,19 @@ impl<'a, 'b> DetectConflictsAndUpdates<'a, 'b> { id: to_rebase_item_weight.id().into(), node_weight_kind: to_rebase_item_weight.into(), }; - conflicts.push(Conflict::ModifyRemovedItem(node_information)) + let container_node_weight = self + .to_rebase_graph + .get_node_weight(to_rebase_container_index)?; + let container_node_information = NodeInformation { + index: to_rebase_container_index, + id: container_node_weight.id().into(), + node_weight_kind: container_node_weight.into(), + }; + + conflicts.push(Conflict::ModifyRemovedItem { + container: container_node_information, + modified_item: node_information, + }); } else { let source_node_weight = self .to_rebase_graph diff --git a/lib/dal/src/workspace_snapshot/graph/tests/detect_conflicts_and_updates.rs b/lib/dal/src/workspace_snapshot/graph/tests/detect_conflicts_and_updates.rs index 46085afbd2..32d06fa872 100644 --- a/lib/dal/src/workspace_snapshot/graph/tests/detect_conflicts_and_updates.rs +++ b/lib/dal/src/workspace_snapshot/graph/tests/detect_conflicts_and_updates.rs @@ -82,6 +82,19 @@ mod test { } } + fn get_root_node_info(graph: &WorkspaceSnapshotGraphV1) -> NodeInformation { + let root_id = graph + .get_node_weight(graph.root_index) + .expect("Unable to get root node") + .id(); + + NodeInformation { + index: graph.root_index, + node_weight_kind: NodeWeightDiscriminants::Content, + id: root_id.into(), + } + } + #[test] fn detect_conflicts_and_updates_simple_no_conflicts_no_updates_in_base() { let actor_id = Ulid::new(); @@ -995,14 +1008,19 @@ mod test { .detect_conflicts_and_updates(new_vector_clock_id, &base_graph, initial_vector_clock_id) .expect("Unable to detect conflicts and updates"); + let container = get_root_node_info(&new_graph); + assert_eq!( - vec![Conflict::ModifyRemovedItem(NodeInformation { - id: component_id.into(), - index: new_graph - .get_node_index_by_id(component_id) - .expect("Unable to get NodeIndex"), - node_weight_kind: NodeWeightDiscriminants::Content, - })], + vec![Conflict::ModifyRemovedItem { + container, + modified_item: NodeInformation { + id: component_id.into(), + index: new_graph + .get_node_index_by_id(component_id) + .expect("Unable to get NodeIndex"), + node_weight_kind: NodeWeightDiscriminants::Content, + } + }], conflicts_and_updates.conflicts ); assert!(conflicts_and_updates.updates.is_empty()); @@ -1135,17 +1153,22 @@ mod test { .detect_conflicts_and_updates(vector_clock_id, &base_graph, vector_clock_id) .expect("Unable to detect conflicts and updates"); + let container = get_root_node_info(&new_graph); + // Even though we have identical vector clocks, this still produces a // conflict, since this item has been modified in to_rebase after onto // removed it. assert_eq!( - vec![Conflict::ModifyRemovedItem(NodeInformation { - id: component_id.into(), - index: new_graph - .get_node_index_by_id(component_id) - .expect("Unable to get NodeIndex"), - node_weight_kind: NodeWeightDiscriminants::Content, - })], + vec![Conflict::ModifyRemovedItem { + container, + modified_item: NodeInformation { + id: component_id.into(), + index: new_graph + .get_node_index_by_id(component_id) + .expect("Unable to get NodeIndex"), + node_weight_kind: NodeWeightDiscriminants::Content, + } + }], conflicts_and_updates.conflicts ); assert!(conflicts_and_updates.updates.is_empty()); @@ -1672,15 +1695,19 @@ mod test { // base_graph.dot(); // new_graph.dot(); + let container = get_root_node_info(&new_graph); let expected_conflicts = vec![ - Conflict::ModifyRemovedItem(NodeInformation { - index: new_graph - .get_node_index_by_id(nginx_butane_component_id) - .expect("Unable to get component NodeIndex"), - id: nginx_butane_component_id.into(), - node_weight_kind: NodeWeightDiscriminants::Content, - }), + Conflict::ModifyRemovedItem { + container, + modified_item: NodeInformation { + index: new_graph + .get_node_index_by_id(nginx_butane_component_id) + .expect("Unable to get component NodeIndex"), + id: nginx_butane_component_id.into(), + node_weight_kind: NodeWeightDiscriminants::Content, + }, + }, Conflict::NodeContent { onto: NodeInformation { index: base_graph @@ -4063,15 +4090,8 @@ mod test { assert!(updates.is_empty()); assert_eq!(1, conflicts.len()); - let container = NodeInformation { - index: to_rebase_graph.root_index, - id: to_rebase_graph - .get_node_weight(to_rebase_graph.root()) - .expect("Unable to get root node") - .id() - .into(), - node_weight_kind: NodeWeightDiscriminants::Content, - }; + let container = get_root_node_info(&to_rebase_graph); + let removed_index = onto_graph .get_node_index_by_id(prototype_node_id) .expect("get_node_index_by_id"); @@ -4097,7 +4117,14 @@ mod test { .detect_conflicts_and_updates(vector_clock_id, &to_rebase_graph, vector_clock_id) .expect("detect_conflicts_and_updates"); assert!(updates.is_empty()); - assert_eq!(Conflict::ModifyRemovedItem(removed_item), conflicts[0]); + let container = get_root_node_info(&onto_graph); + assert_eq!( + Conflict::ModifyRemovedItem { + container, + modified_item: removed_item + }, + conflicts[0] + ); } #[test] diff --git a/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs index b8b22c907a..2ea6e70e35 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs @@ -3,6 +3,8 @@ use si_events::merkle_tree_hash::MerkleTreeHash; use si_events::VectorClockId; use si_events::{ulid::Ulid, ContentHash}; +use super::deprecated::DeprecatedContentNodeWeight; +use crate::workspace_snapshot::content_address::ContentAddressDiscriminants; use crate::workspace_snapshot::{ content_address::ContentAddress, graph::LineageId, @@ -11,8 +13,6 @@ use crate::workspace_snapshot::{ }; use crate::EdgeWeightKindDiscriminants; -use super::deprecated::DeprecatedContentNodeWeight; - #[derive(Clone, Serialize, Deserialize)] pub struct ContentNodeWeight { /// The stable local ID of the object in question. Mainly used by external things like @@ -63,6 +63,10 @@ impl ContentNodeWeight { self.content_address } + pub fn content_address_discriminants(&self) -> ContentAddressDiscriminants { + self.content_address.into() + } + pub fn content_hash(&self) -> ContentHash { self.content_address.content_hash() } diff --git a/lib/sdf-server/src/server/service/component.rs b/lib/sdf-server/src/server/service/component.rs index 7c7a2eb95f..33340eb1b9 100644 --- a/lib/sdf-server/src/server/service/component.rs +++ b/lib/sdf-server/src/server/service/component.rs @@ -10,6 +10,7 @@ use dal::validation::ValidationError; use dal::{ action::prototype::ActionPrototypeError, action::ActionError, ComponentError as DalComponentError, FuncError, StandardModelError, WorkspaceError, + WorkspaceSnapshotError, }; use dal::{ attribute::value::debug::AttributeDebugViewError, component::ComponentId, PropId, @@ -20,6 +21,7 @@ use dal::{ChangeSetError, TransactionsError}; use thiserror::Error; use crate::server::state::AppState; +use crate::service::component::conflicts_for_component::conflicts_for_component; pub mod delete_property_editor_value; pub mod get_actions; @@ -34,6 +36,7 @@ pub mod update_property_editor_value; // pub mod list_resources; pub mod refresh; // pub mod resource_domain_diff; +pub mod conflicts_for_component; pub mod debug; pub mod get_code; pub mod restore_default_function; @@ -95,6 +98,8 @@ pub enum ComponentError { ValidationResolver(#[from] ValidationError), #[error("workspace error: {0}")] Workspace(#[from] WorkspaceError), + #[error("workspace snapshot error: {0}")] + WorkspaceSnapshot(#[from] WorkspaceSnapshotError), #[error("ws event error: {0}")] WsEvent(#[from] WsEventError), } @@ -164,4 +169,5 @@ pub fn routes() -> Router { .route("/debug", get(debug::debug_component)) .route("/json", get(json::json)) .route("/upgrade_component", post(upgrade::upgrade)) + .route("/conflicts", get(conflicts_for_component)) } diff --git a/lib/sdf-server/src/server/service/component/conflicts_for_component.rs b/lib/sdf-server/src/server/service/component/conflicts_for_component.rs new file mode 100644 index 0000000000..eff91a7f9e --- /dev/null +++ b/lib/sdf-server/src/server/service/component/conflicts_for_component.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use axum::Json; +use dal::{ + workspace_snapshot::conflict::Conflict, AttributeValue, AttributeValueId, ChangeSet, + ComponentId, Visibility, WorkspaceSnapshot, WorkspaceSnapshotError, +}; +use serde::{Deserialize, Serialize}; +use ulid::Ulid; + +use crate::server::extract::{AccessBuilder, HandlerContext}; +use crate::service::component::ComponentResult; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ConflictsForComponentRequest { + pub component_id: ComponentId, + #[serde(flatten)] + pub visibility: Visibility, +} + +pub type ConflictsForComponentResponse = + HashMap; + +// TODO get visibility and component id via path as the V2 endpoints do +pub async fn conflicts_for_component( + HandlerContext(builder): HandlerContext, + AccessBuilder(request_ctx): AccessBuilder, + Json(ConflictsForComponentRequest { + component_id, + visibility, + }): Json, +) -> ComponentResult> { + let ctx = builder.build(request_ctx.build(visibility)).await?; + + // Get change set snapshot + let change_set = ChangeSet::find(&ctx, visibility.change_set_id) + .await? + .ok_or(dal::ChangeSetError::ChangeSetNotFound( + visibility.change_set_id, + ))?; + let cs_workspace_snapshot = WorkspaceSnapshot::find_for_change_set(&ctx, change_set.id).await?; + let cs_vector_clock_id = cs_workspace_snapshot + .max_recently_seen_clock_id(Some(change_set.id)) + .await? + .ok_or(WorkspaceSnapshotError::RecentlySeenClocksMissing( + change_set.id, + ))?; + + // Get base snapshot + let base_change_set = if let Some(base_change_set_id) = change_set.base_change_set_id { + ChangeSet::find(&ctx, base_change_set_id) + .await? + .ok_or(dal::ChangeSetError::ChangeSetNotFound(base_change_set_id))? + } else { + return Err(dal::ChangeSetError::NoBaseChangeSet(visibility.change_set_id).into()); + }; + let base_snapshot = WorkspaceSnapshot::find_for_change_set(&ctx, base_change_set.id).await?; + let base_vector_clock_id = base_snapshot + .max_recently_seen_clock_id(Some(base_change_set.id)) + .await? + .ok_or(WorkspaceSnapshotError::RecentlySeenClocksMissing( + base_change_set.id, + ))?; + + let conflicts_and_updates_change_set_into_base = base_snapshot + .detect_conflicts_and_updates( + base_vector_clock_id, + &cs_workspace_snapshot, + cs_vector_clock_id, + ) + .await?; + + // TODO move this to the dal for ease of testing and write tests + let mut conflicts_for_av_id = ConflictsForComponentResponse::new(); + + for conflict in conflicts_and_updates_change_set_into_base.conflicts { + let node_index = cs_workspace_snapshot + .get_node_index_by_id(node_ulid_for_conflict(conflict)) + .await?; + + let node_weight = cs_workspace_snapshot.get_node_weight(node_index).await?; + + let Some(this_av_id) = cs_workspace_snapshot + .associated_attribute_value_id(node_weight) + .await? + else { + continue; + }; + + if AttributeValue::component_id(&ctx, this_av_id).await? != component_id { + continue; + } + + let frontend_conflict = match conflict { + Conflict::ChildOrder { .. } + | Conflict::ExclusiveEdgeMismatch { .. } + | Conflict::NodeContent { .. } => si_frontend_types::ConflictWithHead::Untreated { + raw: serde_json::json!(conflict).to_string(), + }, + + Conflict::ModifyRemovedItem { .. } => { + si_frontend_types::ConflictWithHead::RemovedWhatHeadModified { + container_av_id: this_av_id.into(), + } + } + + Conflict::RemoveModifiedItem { .. } => { + si_frontend_types::ConflictWithHead::ModifiedWhatHeadRemoved { + modified_av_id: this_av_id.into(), + } + } + }; + + conflicts_for_av_id.insert(this_av_id, frontend_conflict); + } + + Ok(Json(conflicts_for_av_id)) +} + +fn node_ulid_for_conflict(conflict: Conflict) -> Ulid { + match conflict { + Conflict::ChildOrder { onto, .. } => onto.id, + Conflict::ExclusiveEdgeMismatch { destination, .. } => destination.id, + Conflict::ModifyRemovedItem { container, .. } => container.id, + Conflict::NodeContent { onto, .. } => onto.id, + Conflict::RemoveModifiedItem { removed_item, .. } => removed_item.id, + } + .into() +} diff --git a/lib/si-frontend-types-rs/src/conflict.rs b/lib/si-frontend-types-rs/src/conflict.rs new file mode 100644 index 0000000000..3e30e7eb94 --- /dev/null +++ b/lib/si-frontend-types-rs/src/conflict.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; +use si_events::AttributeValueId; +use strum::{AsRefStr, Display, EnumIter, EnumString}; + +#[remain::sorted] +#[derive( + AsRefStr, + Clone, + Debug, + Deserialize, + EnumString, + Eq, + Serialize, + Display, + EnumIter, + PartialEq, + Hash, +)] +#[serde(rename_all = "camelCase", tag = "bindingKind")] +pub enum ConflictWithHead { + #[serde(rename_all = "camelCase")] + ModifiedWhatHeadRemoved { modified_av_id: AttributeValueId }, + #[serde(rename_all = "camelCase")] + RemovedWhatHeadModified { container_av_id: AttributeValueId }, + #[serde(rename_all = "camelCase")] + Untreated { raw: String }, +} diff --git a/lib/si-frontend-types-rs/src/lib.rs b/lib/si-frontend-types-rs/src/lib.rs index 719f84f326..6c91d8bd13 100644 --- a/lib/si-frontend-types-rs/src/lib.rs +++ b/lib/si-frontend-types-rs/src/lib.rs @@ -1,7 +1,9 @@ +mod conflict; mod func; mod module; mod schema_variant; +pub use crate::conflict::ConflictWithHead; pub use crate::func::{ AttributeArgumentBinding, FuncArgument, FuncArgumentKind, FuncBinding, FuncBindings, FuncCode, FuncSummary, LeafInputLocation,