Skip to main content
Glama
component.rs127 kB
//! This module contains [`Component`], which is an instance of a //! [`SchemaVariant`](SchemaVariant) and a _model_ of a "real world resource". use std::{ collections::{ HashMap, HashSet, VecDeque, hash_map, }, num::{ ParseFloatError, ParseIntError, }, str::FromStr, sync::Arc, }; use resource::ResourceData; use serde::{ Deserialize, Serialize, }; use si_db::{ ActorView, HistoryEventMetadata, }; use si_events::{ ContentHash, Timestamp, ulid::Ulid, }; use si_frontend_types::{ DiagramComponentView, DiagramSocket, DiagramSocketDirection, DiagramSocketNodeSide, GeometryAndView, RawGeometry, }; use si_id::SchemaId; use si_pkg::KeyOrIndex; use si_split_graph::SplitGraphError; use socket::{ ComponentInputSocket, ComponentOutputSocket, }; use telemetry::prelude::*; use thiserror::Error; use tokio::sync::TryLockError; use self::suggestion::PropSuggestionCacheError; use crate::{ AttributePrototype, AttributePrototypeId, AttributeValue, AttributeValueId, ChangeSetId, DalContext, EdgeWeight, Func, FuncError, FuncId, HelperError, InputSocket, InputSocketId, OutputSocket, OutputSocketId, Prop, PropId, PropKind, Schema, SchemaVariant, SchemaVariantId, SocketArity, TransactionsError, WorkspaceError, WorkspacePk, WsEvent, WsEventError, WsEventResult, WsPayload, action::{ Action, ActionError, ActionState, prototype::{ ActionKind, ActionPrototype, ActionPrototypeError, }, }, attribute::{ attributes::{ self, AttributeValueIdent, Source, }, path::AttributePath, prototype::{ AttributePrototypeError, argument::{ AttributePrototypeArgument, AttributePrototypeArgumentError, AttributePrototypeArgumentId, static_value::StaticArgumentValue, value_source::ValueSource, }, }, value::{ AttributeValueError, ChildAttributeValuePair, DependentValueGraph, ValueIsFor, subscription::ValueSubscription, }, }, change_set::ChangeSetError, change_status::ChangeStatus, code_view::CodeViewError, diagram::{ DiagramError, SummaryDiagramManagementEdge, geometry::Geometry, view::{ View, ViewId, }, }, entity_kind::EntityKindError, func::{ argument::FuncArgumentError, binding::FuncBindingError, intrinsics::IntrinsicFunc, leaf::LeafKind, }, implement_add_edge_to, layer_db_types::{ ComponentContent, ComponentContentV2, }, module::{ Module, ModuleError, }, prop::{ PropError, PropPath, }, qualification::{ QualificationError, QualificationSummaryError, }, schema::variant::{ SchemaVariantError, root_prop::component_type::ComponentType, }, socket::{ input::InputSocketError, output::OutputSocketError, }, validation::ValidationError, workspace_snapshot::{ DependentValueRoot, WorkspaceSnapshotError, dependent_value_root::DependentValueRootError, edge_weight::{ EdgeWeightKind, EdgeWeightKindDiscriminants, }, graph::WorkspaceSnapshotGraphError, node_weight::{ ComponentNodeWeight, NodeWeight, NodeWeightError, category_node_weight::CategoryNodeKind, }, traits::attribute_value::AttributeValueExt as _, }, }; pub mod code; pub mod debug; pub mod delete; pub mod diff; pub mod new; pub mod properties; pub mod qualification; pub mod resource; pub mod socket; pub mod suggestion; #[remain::sorted] #[derive(Debug, Error)] pub enum ComponentError { #[error("action error: {0}")] Action(#[from] Box<ActionError>), #[error("action prototype error: {0}")] ActionPrototype(#[from] Box<ActionPrototypeError>), #[error("attribute prototype error: {0}")] AttributePrototype(#[from] Box<AttributePrototypeError>), #[error("attribute prototype argument error: {0}")] AttributePrototypeArgument(#[from] Box<AttributePrototypeArgumentError>), #[error("attributes error: {0}")] Attributes(#[from] Box<attributes::AttributesError>), #[error("attribute value error: {0}")] AttributeValue(#[from] Box<AttributeValueError>), #[error( "attribute value view could not be generated, root value not found; workspace_pk={0}, change_set_id={1}, component_id={2}" )] AttributeValueView(WorkspacePk, ChangeSetId, ComponentId), #[error("cannot clone attributes from a component with a different schema variant id")] CannotCloneFromDifferentVariants, #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), #[error("code view error: {0}")] CodeView(#[from] Box<CodeViewError>), #[error("component {0} already has a geometry for view {1}")] ComponentAlreadyInView(ComponentId, ViewId), #[error("component {0} has more than one value for the {1} prop")] ComponentHasTooManyValues(ComponentId, PropId), #[error("component {0} has an unexpected schema variant id")] ComponentIncorrectSchemaVariant(ComponentId), #[error("component {0} has no attribute value for the root/si/color prop")] ComponentMissingColorValue(ComponentId), #[error("component {0} has no attribute value for the root/si/name prop")] ComponentMissingNameValue(ComponentId), #[error("component {0} has no attribute value for the root/resource prop")] ComponentMissingResourceValue(ComponentId), #[error("component {0} has no attribute value for the root/si/type prop")] ComponentMissingTypeValue(ComponentId), #[error("component {0} has no materialized view for the root/si/type prop")] ComponentMissingTypeValueMaterializedView(ComponentId), #[error("component {0} has no attribute value for the {1} prop")] ComponentMissingValue(ComponentId, PropId), #[error("dependent value root error: {0}")] DependentValueRoot(#[from] DependentValueRootError), #[error("connection destination component {0} has no attribute value for input socket {1}")] DestinationComponentMissingAttributeValueForInputSocket(ComponentId, InputSocketId), #[error("diagram error: {0}")] Diagram(#[from] Box<DiagramError>), #[error("entity kind error: {0}")] EntityKind(#[from] Box<EntityKindError>), #[error("func error: {0}")] Func(#[from] Box<FuncError>), #[error("func argument error: {0}")] FuncArgumentError(#[from] Box<FuncArgumentError>), #[error("func binding error: {0}")] FuncBinding(#[from] Box<FuncBindingError>), #[error("helper error: {0}")] Helper(#[from] HelperError), #[error("input socket error: {0}")] InputSocket(#[from] Box<InputSocketError>), #[error("input socket {0} not found for component id {1}")] InputSocketNotFoundForComponentId(InputSocketId, ComponentId), #[error("input socket {0} has more than one attribute value")] InputSocketTooManyAttributeValues(InputSocketId), #[error("layer db error: {0}")] LayerDb(#[from] si_layer_cache::LayerDbError), #[error("component {0} missing attribute value for code")] MissingCodeValue(ComponentId), #[error("missing controlling func data for parent attribute value id: {0}")] MissingControllingFuncDataForParentAttributeValue(AttributeValueId), #[error("missing path for attribute value: {0}")] MissingPathForAttributeValue(AttributeValueId), #[error("component {0} missing attribute value for qualifications")] MissingQualificationsValue(ComponentId), #[error("component {0} missing attribute value for root")] MissingRootProp(ComponentId), #[error("module error: {0}")] Module(#[from] Box<ModuleError>), #[error("more than one schema variant found for component: {0}")] MoreThanOneSchemaVariantFound(ComponentId), #[error("found multiple parents for component: {0}")] MultipleParentsForComponent(ComponentId), #[error("found multiple root attribute values ({0} and {1}, at minimum) for component: {2}")] MultipleRootAttributeValuesFound(AttributeValueId, AttributeValueId, ComponentId), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), #[error("component not found: {0}")] NotFound(ComponentId), #[error("component not found by name: {0}")] NotFoundByName(String), #[error("object prop {0} has no ordering node")] ObjectPropHasNoOrderingNode(PropId), #[error("output socket error: {0}")] OutputSocket(#[from] Box<OutputSocketError>), #[error("output socket has not found for attribute value id {0}")] OutputSocketNotFoundForAttributeValueId(AttributeValueId), #[error("output socket {0} not found for component id {1}")] OutputSocketNotFoundForComponentId(OutputSocketId, ComponentId), #[error("output socket {0} has more than one attribute value")] OutputSocketTooManyAttributeValues(OutputSocketId), #[error("parse float error: {0}")] ParseFloat(#[from] ParseFloatError), #[error("parse int error: {0}")] ParseInt(#[from] ParseIntError), #[error("prop error: {0}")] Prop(#[from] Box<PropError>), #[error("found prop id ({0}) that is not a prop")] PropIdNotAProp(PropId), #[error("prop suggestion cache error: {0}")] PropSuggestionCache(#[from] PropSuggestionCacheError), #[error("qualification error: {0}")] Qualification(#[from] Box<QualificationError>), #[error("ordering node not found for qualifications map {0} and component {1}")] QualificationNoOrderingNode(AttributeValueId, ComponentId), #[error("qualification summary error: {0}")] QualificationSummary(#[from] Box<QualificationSummaryError>), #[error("resource attribute value not found for component: {0}")] ResourceAttributeValueNotFound(ComponentId), #[error("root attribute value not found for component: {0}")] RootAttributeValueNotFound(ComponentId), #[error("schema variant error: {0}")] SchemaVariant(#[from] Box<SchemaVariantError>), #[error("schema variant not found for component: {0}")] SchemaVariantNotFound(ComponentId), #[error("serde_json error: {0}")] Serde(#[from] serde_json::Error), #[error("si db error: {0}")] SiDb(#[from] si_db::Error), #[error("split graph error: {0}")] SplitGraph(#[from] SplitGraphError), #[error("transactions error: {0}")] Transactions(#[from] TransactionsError), #[error("try lock error: {0}")] TryLock(#[from] TryLockError), #[error("ulid decode error: {0}")] Ulid(#[from] ulid::DecodeError), #[error("validation error: {0}")] Validation(#[from] Box<ValidationError>), #[error("value source for known prop attribute value {0} is not a prop id")] ValueSourceForPropValueNotPropId(AttributeValueId), #[error("workspace error: {0}")] Workspace(#[from] Box<WorkspaceError>), #[error("workspace pk not found on context")] WorkspacePkNone, #[error("workspace snapshot error: {0}")] WorkspaceSnapshot(#[from] WorkspaceSnapshotError), #[error("workspace snapshot graph error: {0}")] WorkspaceSnapshotGraphError(#[from] WorkspaceSnapshotGraphError), #[error("attribute value {0} has wrong type for operation: {0}")] WrongAttributeValueType(AttributeValueId, ValueIsFor), #[error("Attribute Prototype Argument used by too many Attribute Prototypes: {0}")] WrongNumberOfPrototypesForAttributePrototypeArgument(AttributePrototypeArgumentId), #[error("WsEvent error: {0}")] WsEvent(#[from] Box<WsEventError>), } impl From<ActionError> for ComponentError { fn from(err: ActionError) -> Self { Box::new(err).into() } } impl From<ActionPrototypeError> for ComponentError { fn from(err: ActionPrototypeError) -> Self { Box::new(err).into() } } impl From<DiagramError> for ComponentError { fn from(err: DiagramError) -> Self { Box::new(err).into() } } impl From<EntityKindError> for ComponentError { fn from(err: EntityKindError) -> Self { Box::new(err).into() } } impl From<FuncBindingError> for ComponentError { fn from(err: FuncBindingError) -> Self { Box::new(err).into() } } impl From<AttributePrototypeError> for ComponentError { fn from(value: AttributePrototypeError) -> Self { Box::new(value).into() } } impl From<AttributePrototypeArgumentError> for ComponentError { fn from(value: AttributePrototypeArgumentError) -> Self { Box::new(value).into() } } impl From<AttributeValueError> for ComponentError { fn from(value: AttributeValueError) -> Self { Box::new(value).into() } } impl From<CodeViewError> for ComponentError { fn from(value: CodeViewError) -> Self { Box::new(value).into() } } impl From<FuncError> for ComponentError { fn from(value: FuncError) -> Self { Box::new(value).into() } } impl From<FuncArgumentError> for ComponentError { fn from(value: FuncArgumentError) -> Self { Box::new(value).into() } } impl From<InputSocketError> for ComponentError { fn from(value: InputSocketError) -> Self { Box::new(value).into() } } impl From<ModuleError> for ComponentError { fn from(value: ModuleError) -> Self { Box::new(value).into() } } impl From<OutputSocketError> for ComponentError { fn from(value: OutputSocketError) -> Self { Box::new(value).into() } } impl From<PropError> for ComponentError { fn from(value: PropError) -> Self { Box::new(value).into() } } impl From<QualificationError> for ComponentError { fn from(value: QualificationError) -> Self { Box::new(value).into() } } impl From<SchemaVariantError> for ComponentError { fn from(value: SchemaVariantError) -> Self { Box::new(value).into() } } impl From<ValidationError> for ComponentError { fn from(value: ValidationError) -> Self { Box::new(value).into() } } impl From<WorkspaceError> for ComponentError { fn from(value: WorkspaceError) -> Self { Box::new(value).into() } } impl From<WsEventError> for ComponentError { fn from(value: WsEventError) -> Self { Box::new(value).into() } } impl From<attributes::AttributesError> for ComponentError { fn from(value: attributes::AttributesError) -> Self { Box::new(value).into() } } pub type ComponentResult<T> = Result<T, ComponentError>; pub use si_id::ComponentId; /// A [`Component`] is an instantiation of a [`SchemaVariant`](crate::SchemaVariant). #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct Component { id: ComponentId, #[serde(flatten)] timestamp: Timestamp, to_delete: bool, } impl From<Component> for ComponentContentV2 { fn from(value: Component) -> Self { Self { timestamp: value.timestamp, } } } #[derive(Copy, Clone, Debug)] pub struct ControllingFuncData { pub func_id: FuncId, pub av_id: AttributeValueId, pub is_dynamic_func: bool, } impl Component { pub fn assemble(node_weight: &ComponentNodeWeight, content: ComponentContentV2) -> Self { Self { id: node_weight.id().into(), timestamp: content.timestamp, to_delete: node_weight.to_delete(), } } pub fn id(&self) -> ComponentId { self.id } pub fn timestamp(&self) -> &Timestamp { &self.timestamp } pub fn to_delete(&self) -> bool { self.to_delete } pub async fn change_status(&self, ctx: &DalContext) -> ComponentResult<ChangeStatus> { let status = if self.exists_on_head(ctx).await? { if self.to_delete() { ChangeStatus::Deleted } else { ChangeStatus::Unmodified } } else { ChangeStatus::Added }; Ok(status) } pub async fn exists_on_head(&self, ctx: &DalContext) -> ComponentResult<bool> { Self::exists_on_head_by_id(ctx, self.id).await } async fn exists_on_head_by_id(ctx: &DalContext, id: ComponentId) -> ComponentResult<bool> { let head_ctx = ctx.clone_with_head().await?; Self::exists_by_id(&head_ctx, id).await } pub async fn view(&self, ctx: &DalContext) -> ComponentResult<Option<serde_json::Value>> { Self::view_by_id(ctx, self.id).await } pub async fn view_by_id( ctx: &DalContext, id: ComponentId, ) -> ComponentResult<Option<serde_json::Value>> { let schema_variant_id = Self::schema_variant_id(ctx, id).await?; let root_prop_id = Prop::find_prop_id_by_path(ctx, schema_variant_id, &PropPath::new(["root"])).await?; for value_id in Component::attribute_values_for_prop_id(ctx, id, root_prop_id).await? { let value_component_id = AttributeValue::component_id(ctx, value_id).await?; if value_component_id == id { return Ok(ctx .workspace_snapshot()? .attribute_value_view(ctx, value_id) .await?); } } // Should this be an error? Ok(None) } implement_add_edge_to!( source_id: ComponentId, destination_id: SchemaVariantId, add_fn: add_edge_to_schema_variant, discriminant: EdgeWeightKindDiscriminants::Use, result: ComponentResult, ); implement_add_edge_to!( source_id: ComponentId, destination_id: AttributeValueId, add_fn: add_edge_to_root_attribute_value, discriminant: EdgeWeightKindDiscriminants::Root, result: ComponentResult, ); implement_add_edge_to!( source_id: ComponentId, destination_id: AttributeValueId, add_fn: add_edge_to_socket_attribute_value, discriminant: EdgeWeightKindDiscriminants::SocketValue, result: ComponentResult, ); implement_add_edge_to!( source_id: Ulid, destination_id: ComponentId, add_fn: add_category_edge, discriminant: EdgeWeightKindDiscriminants::Use, result: ComponentResult, ); implement_add_edge_to!( source_id: ComponentId, destination_id: ComponentId, add_fn: add_manages_edge_to_component, discriminant: EdgeWeightKindDiscriminants::Manages, result: ComponentResult, ); /// Attempts to merge the values other_component into this component, if /// values exist for the prop in other. Only use this immediately after /// Component::new, so that we can make certain assumptions (for example, we /// can assume that the prototypes are correct, and that arrays and maps are /// empty) async fn merge_from_component_with_different_schema_variant( &self, ctx: &DalContext, old_component_id: ComponentId, ) -> ComponentResult<()> { let old_root_id = Component::root_attribute_value_id(ctx, old_component_id).await?; let self_schema_variant_id = Component::schema_variant_id(ctx, self.id).await?; let mut dvu_roots = vec![]; // Gather a bunch of data about the current schema variant let mut new_input_sockets = HashMap::new(); for input_socket_id in InputSocket::list_ids_for_schema_variant(ctx, self_schema_variant_id).await? { let input_socket = InputSocket::get_by_id(ctx, input_socket_id).await?; new_input_sockets.insert(input_socket.name().to_string(), input_socket.id()); } let mut new_output_sockets = HashMap::new(); for output_socket_id in OutputSocket::list_ids_for_schema_variant(ctx, self_schema_variant_id).await? { let output_socket = OutputSocket::get_by_id(ctx, output_socket_id).await?; new_output_sockets.insert(output_socket.name().to_string(), output_socket.id()); } let mut new_props = HashMap::new(); for prop in SchemaVariant::all_props(ctx, self_schema_variant_id).await? { let path = prop.path(ctx).await?; new_props.insert(path.as_owned_parts(), prop.id()); } // Walk the original components attribute value tree, finding matching // values in self and updating their value if necessary. Also find if a // component specific dynamic function was configured in the original // component. If so, attempt to copy it over. let mut value_q = VecDeque::from([(old_root_id, None, None)]); while let Some((old_av_id, old_key_or_index, new_parent_id)) = value_q.pop_front() { let old_av = AttributeValue::get_by_id(ctx, old_av_id).await?; let maybe_old_component_prototype_id = AttributeValue::component_prototype_id(ctx, old_av_id).await?; let old_prop_id = AttributeValue::is_for(ctx, old_av_id) .await? .prop_id() .ok_or(ComponentError::ValueSourceForPropValueNotPropId(old_av_id))?; let prop_path = Prop::path_by_id(ctx, old_prop_id).await?.as_owned_parts(); // Is there a matching prop in self for this prop in other? If there // is no matching prop do nothing (this means the prop was removed // from self, so can't get values from other) let Some(&new_prop_id) = new_props.get(&prop_path) else { continue; }; let new_prop = Prop::get_by_id(ctx, new_prop_id).await?; let old_prop = Prop::get_by_id(ctx, old_prop_id).await?; // Prop kinds could have changed for the same prop. We could // try and coerce values, but it's safer to just skip. Even if // there is a component specific prototype for this prop's value // in other, we don't want to copy it over, since the kind has // changed. if new_prop.kind != old_prop.kind { continue; } // Similarly, we should verify that the secret kind has not // changed if this is a secret prop. If it has changed, leave // the prop alone (effectively emptying the secret) if new_prop.secret_kind_widget_option() != old_prop.secret_kind_widget_option() { continue; } // If there is another av for this prop with the same path, get that to populate later let maybe_new_av_id = { let old_av_path = AttributeValue::get_path_for_id(ctx, old_av_id).await?; let mut new_av_id = None; for av_id_for_prop in Component::attribute_values_for_prop_id(ctx, self.id, new_prop_id).await? { let new_av_path = AttributeValue::get_path_for_id(ctx, av_id_for_prop).await?; if old_av_path == new_av_path { new_av_id = Some(av_id_for_prop); } } new_av_id }; let key = old_key_or_index .as_ref() .and_then(|key_or_index| match key_or_index { KeyOrIndex::Key(key) => Some(key.to_owned()), _ => None, }); let new_av_id = match maybe_new_av_id { // The value exists in both old and new (thought it might be defaulted) Some(new_av_id) => { match maybe_old_component_prototype_id { // The old component has an explicit value set rather than using // the default: set the value in the new component as well. Some(old_component_prototype_id) => { let old_func_id = AttributePrototype::func_id(ctx, old_component_prototype_id) .await?; if Func::is_dynamic(ctx, old_func_id).await? { // a custom function has been defined for // this specific component. We have to copy // this custom prototype over, but we can // only do so if the inputs to the function // exist in self after regeneration and have // the same types. self.merge_component_specific_dynamic_func_from_other( ctx, new_av_id, old_component_prototype_id, &new_input_sockets, &new_output_sockets, &new_props, key.clone(), ) .await?; // This AV was modified by copying a dynamic function, so it needs DVU dvu_roots.push(DependentValueRoot::Unfinished(new_av_id.into())); // We continue here since we don't want to descend below a dynamic func continue; } else { // Ok, the original component has a // component specific prototype here, but // it's not a dynamic function. Just set the // value. This means either it's a simple // scalar that has had a value set manually, // *OR*, it's a value set by a dynamic // function that has been overriden by the // user, manually, either way, we want to // just set the value let old_value = old_av.value(ctx).await?; AttributeValue::set_value(ctx, new_av_id, old_value).await?; } } // The old component was using the default value. The new component // should do the same, so there's not much to do, except for root/si/type! None => { // The only exception is values that change the meaning or // validity of other components and connections the user may // have created. In these cases, we want to preserve the old // value to prevent the user's work from being invalidated. // // For example, if root/si/type is changed from Frame to // Component, and the user had already added child components, // those child components would now be in an invalid place // (because Components can't have children). // // If root/si/type is not set by a component specific prototype, // this means the component was created before we were always setting // a component specific prototype for components. If we hit this, // just set the value here and now so it will have a component specific prototype // from here on out. // // If for whatever reason, there isn't a value set yet for the type, set it to the old // Prop's default value if prop_path == ["root", "si", "type"] { let old_value = old_av.value_or_default_or_null(ctx, old_prop_id).await?; AttributeValue::set_value(ctx, new_av_id, Some(old_value)).await?; // This AV was explicitly modified, so it needs DVU dvu_roots.push(DependentValueRoot::Unfinished(new_av_id.into())); } // But we do need to see if this value is set dynamically. If // it is, we don't want to descend, since the tree underneath // it is completely controlled by the dynamic func. let new_prototype_for_value = AttributeValue::prototype_id(ctx, new_av_id).await?; let new_prototype_func = AttributePrototype::func(ctx, new_prototype_for_value).await?; if Func::is_dynamic(ctx, new_prototype_func.id).await? { continue; } } } new_av_id } // The new schema variant never had the value. If it's an array or map // element, we need to insert it. None => { let Some(old_component_prototype_id) = maybe_old_component_prototype_id else { continue; }; let prototype_func_id = AttributePrototype::func_id(ctx, old_component_prototype_id).await?; // Insert this value let inserted_value = AttributeValue::new( ctx, new_prop_id, Some(self.id), new_parent_id, key.clone(), ) .await?; // If the func for this av is dynamic, it will create its own child avs when // executed, if necessary, so we can skip the rest of the loop if Func::is_dynamic(ctx, prototype_func_id).await? { self.merge_component_specific_dynamic_func_from_other( ctx, inserted_value.id, old_component_prototype_id, &new_input_sockets, &new_output_sockets, &new_props, key.clone(), ) .await?; // We need to make sure this AV makes its way into the DVU roots, even though // we would normally add it at the end of the match arm, because we're // skipping the rest of the match arm by continuing here. dvu_roots.push(DependentValueRoot::Unfinished(inserted_value.id().into())); continue; } // If this av is for an object and it did not exist, it means it's a child of // an array or map. We need to create the children of this object // (and any direct object children) so that we don't get a malformed item in // the new component if new_prop.kind == PropKind::Object { let mut queue: VecDeque<_> = Prop::direct_child_props_ordered(ctx, new_prop_id) .await? .into_iter() .map(|prop| (prop, inserted_value.id)) .collect(); while let Some((this_prop, parent_av_id)) = queue.pop_front() { let attribute_value = AttributeValue::new( ctx, this_prop.id, Some(self.id), Some(parent_av_id), None, ) .await?; for child_prop in Prop::direct_child_props_ordered(ctx, this_prop.id).await? { if child_prop.kind == PropKind::Object { queue.push_back((child_prop, attribute_value.id())) } } } } let old_value = old_av.value(ctx).await?; AttributeValue::set_value(ctx, inserted_value.id, old_value).await?; dvu_roots.push(DependentValueRoot::Unfinished(inserted_value.id.into())); inserted_value.id } }; for old_child_av_id in AttributeValue::get_child_av_ids_in_order(ctx, old_av_id).await? { let old_key_or_index = AttributeValue::get_key_or_index_of_child_entry(ctx, old_child_av_id).await?; value_q.push_back((old_child_av_id, old_key_or_index, Some(new_av_id))); } } let component_graph = DependentValueGraph::new(ctx, dvu_roots).await?; let leaf_value_ids = component_graph.independent_values(); ctx.add_dependent_values_and_enqueue(leaf_value_ids).await?; Ok(()) } #[allow(clippy::too_many_arguments)] async fn merge_component_specific_dynamic_func_from_other( &self, ctx: &DalContext, new_attribute_value_id: AttributeValueId, old_component_prototype_id: AttributePrototypeId, self_input_sockets: &HashMap<String, InputSocketId>, self_output_sockets: &HashMap<String, OutputSocketId>, self_props: &HashMap<Vec<String>, PropId>, key: Option<String>, ) -> ComponentResult<()> { let apa_ids = AttributePrototypeArgument::list_ids_for_prototype(ctx, old_component_prototype_id) .await?; let component_prototype_func_id = AttributePrototype::func_id(ctx, old_component_prototype_id).await?; if !Func::is_dynamic(ctx, component_prototype_func_id).await? { return Ok(()); } let mut new_value_sources = vec![]; for &apa_id in &apa_ids { let func_arg_id = AttributePrototypeArgument::func_argument_id(ctx, apa_id).await?; if let Some(source) = AttributePrototypeArgument::value_source_opt(ctx, apa_id).await? { match source { ValueSource::InputSocket(input_socket_id) => { // find matching input socket in self let input_socket = InputSocket::get_by_id(ctx, input_socket_id).await?; match self_input_sockets.get(input_socket.name()) { Some(self_input_socket_id) => new_value_sources.push(( func_arg_id, ValueSource::InputSocket(*self_input_socket_id), )), None => { // XXX: This means that the dynamic function // XXX: here has an input that no longer exists, so // XXX: we can't copy the function over. // XXX: what should we do here? Warn the user? return Ok(()); } } } ValueSource::OutputSocket(output_socket_id) => { let output_socket = OutputSocket::get_by_id(ctx, output_socket_id).await?; match self_output_sockets.get(output_socket.name()) { Some(self_output_socket_id) => new_value_sources.push(( func_arg_id, ValueSource::OutputSocket(*self_output_socket_id), )), None => { return Ok(()); } } } ValueSource::Prop(prop_id) => { let path = Prop::path_by_id(ctx, prop_id).await?.as_owned_parts(); match self_props.get(&path) { Some(self_prop_id) => new_value_sources .push((func_arg_id, ValueSource::Prop(*self_prop_id))), None => { return Ok(()); } } } ValueSource::Secret(_) | ValueSource::StaticArgumentValue(_) | ValueSource::ValueSubscription(_) => { // Should we determine if this secret is still compatible? new_value_sources.push((func_arg_id, source)); } } } } // All inputs are valid, create the component specific override let new_prototype = AttributePrototype::new(ctx, component_prototype_func_id).await?; for (func_arg_id, value_source) in new_value_sources { AttributePrototypeArgument::new(ctx, new_prototype.id, func_arg_id, value_source) .await?; } AttributeValue::set_component_prototype_id( ctx, new_attribute_value_id, new_prototype.id, key, ) .await?; Ok(()) } /// Copy all the attribute values from old_component_id into this /// component. Components must be on the same schema variant. This will /// preserve any component specific prototypes defined on the component /// being copied from. pub async fn clone_attributes_from( &self, ctx: &DalContext, from_component_id: ComponentId, ) -> ComponentResult<()> { let from_sv_id = Component::schema_variant_id(ctx, from_component_id).await?; let dest_sv_id = Component::schema_variant_id(ctx, self.id).await?; if from_sv_id != dest_sv_id { return Err(ComponentError::CannotCloneFromDifferentVariants); } // Paste attribute value "values" from original component (or create them for maps/arrays) // // We could make this more efficient by skipping everything set by non builtins (si:setString, si:setObject, etc), since everything that is propagated will be re-propagated let from_root_id = Component::root_attribute_value_id(ctx, from_component_id).await?; let dest_root_id = Component::root_attribute_value_id(ctx, self.id).await?; let mut work_queue = VecDeque::from([(from_root_id, dest_root_id)]); // Paste attribute prototypes // - either updates component prototype to a copy of the original component // - or removes component prototype, restoring the schema one (needed because of manual update from the block above) while while let Some((from_av_id, dest_av_id)) = work_queue.pop_front() { AttributeValue::clone_value_from(ctx, dest_av_id, from_av_id).await?; // Get children, possibly creating new ones if we don't have them yet for child_pair in AttributeValue::get_child_av_id_pairs_in_order(ctx, from_av_id, dest_av_id).await? { match child_pair { ChildAttributeValuePair::Both(_, from_child_av_id, dest_child_av_id) => { work_queue.push_back((from_child_av_id, dest_child_av_id)); } // If the child is only in the copied component, we create a new one for // ourselves ChildAttributeValuePair::FirstOnly(key, from_child_av_id) => { let dest_child_av_id = AttributeValue::new( ctx, AttributeValue::is_for(ctx, from_child_av_id).await?, Some(self.id), Some(dest_av_id), key, ) .await? .id; work_queue.push_back((from_child_av_id, dest_child_av_id)); } // TODO this case wasn't handled before, and shouldn't really be possible ... ChildAttributeValuePair::SecondOnly(..) => { continue; } } } } self.clear_resource(ctx).await?; Ok(()) } /// Produce sources for every attribute that has them. /// /// - These are returned in the order they were defined / show up in the UI. /// - If an AV is returned, its children will not be. /// - Subscriptions and actual scalar values are returned. /// pub async fn sources( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Vec<(AttributeValueIdent, Source)>> { let mut sources = vec![]; // Get the root attribute value and load it into the work queue. let root_attribute_value_id = Component::root_attribute_value_id(ctx, component_id).await?; let mut work_queue = Vec::from([root_attribute_value_id]); while let Some(av_id) = work_queue.pop() { // If this attribute value has a source, don't recurse into it. if let Some(source) = Self::attr_to_source(ctx, av_id).await? { let (_, dest_path) = AttributeValue::path_from_root(ctx, av_id).await?; sources.push((dest_path.into(), source)); } else { // Otherwise, push its children so we find their sources as well. let children = AttributeValue::get_child_av_ids_in_order(ctx, av_id).await?; // Load the children onto the end of the work queue, in reverse order, so that // they will be processed first with pop(). work_queue.extend(children.into_iter().rev()); } } Ok(sources) } pub async fn subscription_sources( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Vec<(AttributeValueIdent, Source)>> { let mut sources = Self::sources(ctx, component_id).await?; sources.retain(|(_, source)| matches!(source, Source::Subscription { .. })); Ok(sources) } // Used in sources() async fn attr_to_source( ctx: &DalContext, av_id: AttributeValueId, ) -> ComponentResult<Option<Source>> { // If it's not a component-specific prototype, it has no Source. let Some(prototype_id) = AttributeValue::component_prototype_id(ctx, av_id).await? else { return Ok(None); }; // If the user set this explicitly to si:unset, treat it like it's unspecified. // TODO we should probably return None or Source::Null or something let func_id = AttributePrototype::func_id(ctx, prototype_id).await?; let intrinsic = Func::intrinsic_kind(ctx, func_id).await?; if intrinsic == Some(IntrinsicFunc::Unset) { return Ok(None); } // If it's got multiple or zero arguments, it's a dynamic function we can't handle; // treat it like it's unspecified. // TODO error instead of pretending it has no source! let mut args = AttributePrototypeArgument::list_ids_for_prototype(ctx, prototype_id).await?; let Some(apa_id) = args.pop() else { return Ok(None); }; if !args.is_empty() { return Ok(None); } // Figure out what kind of source this is! let source = match AttributePrototypeArgument::value_source(ctx, apa_id).await? { ValueSource::ValueSubscription(ValueSubscription { attribute_value_id: source_av_id, path, }) => { let source_component_id = AttributeValue::component_id(ctx, source_av_id).await?; let AttributePath::JsonPointer(path) = path; let func_id = AttributePrototype::func_id(ctx, prototype_id).await?; let func = match Func::intrinsic_kind(ctx, func_id).await? { Some(IntrinsicFunc::Identity) => None, _ => Some(func_id.into()), }; Source::Subscription { component: source_component_id.into(), path, func, _keep_existing_subscriptions: None, } } ValueSource::StaticArgumentValue(value_id) => { // If it's a static value, *and* the intrinsic is si:setXXX(), we can return it // as a simple value source. let kind = AttributeValue::prop_kind(ctx, av_id).await?; if intrinsic != Some(kind.intrinsic_set_func()) { // TODO error instead of pretending it has no source! This is a dynamic value return Ok(None); } // If it's an object, array or map, we don't return its value; we look for // childrens' values and return each of those instead! if kind.is_container() { return Ok(None); } Source::Value(StaticArgumentValue::get_by_id(ctx, value_id).await?.value) } _ => { // We don't support anything else // TODO error instead of pretending it has no source! return Ok(None); } }; Ok(Some(source)) } /// Gets the list of subscriptions pointing at this root AV, returning the subscriber AV /// as well as the path they are subscribed to. pub async fn subscribers( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<impl Iterator<Item = (AttributePath, AttributePrototypeArgumentId)>> { let root_av_id = Self::root_attribute_value_id(ctx, component_id).await?; Ok(AttributeValue::subscribers(ctx, root_av_id).await?) } async fn try_get_node_weight_and_content( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Option<(ComponentNodeWeight, ComponentContentV2)>> { if let Some((component_node_weight, content_hash)) = Self::try_get_node_weight_and_content_hash(ctx, component_id).await? { let content: ComponentContent = ctx .layer_db() .cas() .try_read_as(&content_hash) .await? .ok_or(WorkspaceSnapshotError::MissingContentFromStore( component_id.into(), ))?; return Ok(Some((component_node_weight, content.extract()))); } Ok(None) } async fn get_node_weight_and_content( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<(ComponentNodeWeight, ComponentContentV2)> { Self::try_get_node_weight_and_content(ctx, component_id) .await? .ok_or(ComponentError::NotFound(component_id)) } async fn try_get_node_weight_and_content_hash( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Option<(ComponentNodeWeight, ContentHash)>> { let id: Ulid = component_id.into(); if let Some(node_weight) = ctx.workspace_snapshot()?.get_node_weight_opt(id).await { let hash = node_weight.content_hash(); let component_node_weight = node_weight.get_component_node_weight()?; return Ok(Some((component_node_weight, hash))); } Ok(None) } /// Returns whether or not the [`Component`] exists on the underlying graph. pub async fn exists_by_id(ctx: &DalContext, id: ComponentId) -> ComponentResult<bool> { Ok(ctx .workspace_snapshot()? .get_node_weight_opt(id) .await .is_some()) } /// List all IDs for all [`Components`](Component) in the workspace. pub async fn list_ids(ctx: &DalContext) -> ComponentResult<Vec<ComponentId>> { let workspace_snapshot = ctx.workspace_snapshot()?; let component_category_node_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Component) .await?; let component_node_indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( component_category_node_id, EdgeWeightKindDiscriminants::Use, ) .await?; let mut component_ids = Vec::with_capacity(component_node_indices.len()); for index in component_node_indices { let node_weight = workspace_snapshot .get_node_weight(index) .await? .get_component_node_weight()?; component_ids.push(node_weight.id.into()) } component_ids.sort(); Ok(component_ids) } pub async fn list(ctx: &DalContext) -> ComponentResult<Vec<Self>> { let workspace_snapshot = ctx.workspace_snapshot()?; let mut components = vec![]; let component_category_node_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Component) .await?; let component_node_indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( component_category_node_id, EdgeWeightKindDiscriminants::Use, ) .await?; let mut node_weights = vec![]; let mut hashes = vec![]; for index in component_node_indices { let node_weight = workspace_snapshot .get_node_weight(index) .await? .get_component_node_weight()?; hashes.push(node_weight.content_hash()); node_weights.push(node_weight); } let contents: HashMap<ContentHash, ComponentContent> = ctx .layer_db() .cas() .try_read_many_as(hashes.as_slice()) .await?; for node_weight in node_weights { match contents.get(&node_weight.content_hash()) { Some(content) => { components.push(Self::assemble(&node_weight, content.to_owned().extract())); } None => Err(WorkspaceSnapshotError::MissingContentFromStore( node_weight.id(), ))?, } } Ok(components) } pub async fn list_to_be_deleted(ctx: &DalContext) -> ComponentResult<Vec<ComponentId>> { let mut to_be_deleted = vec![]; let components = Self::list(ctx).await?; for component in components { if component.to_delete { to_be_deleted.push(component.id()); } } Ok(to_be_deleted) } pub async fn schema_for_component_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Schema> { let schema_variant = Self::schema_variant_for_component_id(ctx, component_id).await?; Ok(schema_variant.schema(ctx).await?) } pub async fn schema(&self, ctx: &DalContext) -> ComponentResult<Schema> { Self::schema_for_component_id(ctx, self.id).await } pub async fn schema_id_for_component_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<SchemaId> { let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; Ok(SchemaVariant::schema_id(ctx, schema_variant_id).await?) } pub async fn schema_variant_for_component_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<SchemaVariant> { let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; Ok(SchemaVariant::get_by_id(ctx, schema_variant_id).await?) } pub async fn schema_variant(&self, ctx: &DalContext) -> ComponentResult<SchemaVariant> { Self::schema_variant_for_component_id(ctx, self.id).await } pub async fn schema_variant_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<SchemaVariantId> { ctx.workspace_snapshot()? .schema_variant_id_for_component_id(component_id) .await } pub async fn get_by_id(ctx: &DalContext, component_id: ComponentId) -> ComponentResult<Self> { let (node_weight, content) = Self::get_node_weight_and_content(ctx, component_id).await?; Ok(Self::assemble(&node_weight, content)) } pub async fn try_get_by_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Option<Self>> { if let Some((node_weight, content)) = Self::try_get_node_weight_and_content(ctx, component_id).await? { return Ok(Some(Self::assemble(&node_weight, content))); } Ok(None) } pub async fn get_by_name(ctx: &DalContext, name: &str) -> ComponentResult<ComponentId> { Self::find_by_name(ctx, name) .await? .ok_or(ComponentError::NotFoundByName(name.into())) } pub async fn find_by_name( ctx: &DalContext, name: &str, ) -> ComponentResult<Option<ComponentId>> { for component_id in Self::list_ids(ctx).await? { if name == Self::name_by_id(ctx, component_id).await? { return Ok(Some(component_id)); } } Ok(None) } pub async fn geometry(&self, ctx: &DalContext, view_id: ViewId) -> ComponentResult<Geometry> { Ok(Geometry::get_by_component_and_view(ctx, self.id, view_id).await?) } pub async fn set_geometry( &mut self, ctx: &DalContext, view_id: ViewId, x: isize, y: isize, width: Option<isize>, height: Option<isize>, ) -> ComponentResult<Geometry> { let new_geometry = RawGeometry { x, y, width, height, }; self.set_raw_geometry(ctx, new_geometry, view_id).await } pub async fn set_raw_geometry( &mut self, ctx: &DalContext, raw_geometry: RawGeometry, view_id: ViewId, ) -> ComponentResult<Geometry> { let mut geometry_pre = self.geometry(ctx, view_id).await?; if geometry_pre.into_raw() != raw_geometry { geometry_pre.update(ctx, raw_geometry).await?; } Ok(geometry_pre) } pub async fn set_resource_id( &self, ctx: &DalContext, resource_id: &str, ) -> ComponentResult<()> { let path = ["root", "si", "resourceId"]; let sv_id = Self::schema_variant_id(ctx, self.id).await?; let Some(resource_prop_id) = Prop::find_prop_id_by_path_opt(ctx, sv_id, &PropPath::new(path)).await? else { return Ok(()); }; // If the name prop is controlled by an identity or other function, // don't override the prototype here if Prop::is_set_by_dependent_function(ctx, resource_prop_id).await? { return Ok(()); } let av_for_resource_id = Self::attribute_value_for_prop_id(ctx, self.id(), resource_prop_id).await?; AttributeValue::update( ctx, av_for_resource_id, Some(serde_json::to_value(resource_id)?), ) .await?; Ok(()) } pub async fn set_name(&self, ctx: &DalContext, name: &str) -> ComponentResult<()> { let path = ["root", "si", "name"]; let sv_id = Self::schema_variant_id(ctx, self.id).await?; let name_prop_id = Prop::find_prop_id_by_path(ctx, sv_id, &PropPath::new(path)).await?; // If the name prop is controlled by an identity or other function, // don't override the prototype here if Prop::is_set_by_dependent_function(ctx, name_prop_id).await? { return Ok(()); } let av_for_name = Self::attribute_value_for_prop_id(ctx, self.id(), name_prop_id).await?; AttributeValue::update(ctx, av_for_name, Some(serde_json::to_value(name)?)).await?; Ok(()) } pub async fn set_resource( &self, ctx: &DalContext, resource: ResourceData, ) -> ComponentResult<()> { let av_for_resource = Component::attribute_value_for_prop(ctx, self.id(), &["root", "resource"]).await?; AttributeValue::update(ctx, av_for_resource, Some(serde_json::to_value(resource)?)).await?; Ok(()) } pub async fn clear_resource(&self, ctx: &DalContext) -> ComponentResult<()> { let av_for_resource = Component::attribute_value_for_prop(ctx, self.id(), &["root", "resource"]).await?; AttributeValue::update(ctx, av_for_resource, Some(serde_json::json!({}))).await?; Ok(()) } /// Finds the [`ResourceData`] for a given [`Component`]. pub async fn resource(&self, ctx: &DalContext) -> ComponentResult<Option<ResourceData>> { Self::resource_by_id(ctx, self.id).await } /// Finds the [`ResourceData`] for a given [`ComponentId`](Component). pub async fn resource_by_id( ctx: &DalContext, id: ComponentId, ) -> ComponentResult<Option<ResourceData>> { let value_id = Self::attribute_value_for_prop(ctx, id, &["root", "resource"]).await?; match AttributeValue::view(ctx, value_id).await? { Some(serde_value) => { if serde_value.is_object() && serde_value .as_object() .expect("we just checked if its an object") .is_empty() { Ok(None) } else { Ok(Some(serde_json::from_value(serde_value)?)) } } None => Ok(None), } } /// Returns the name of a [`Component`] for a given [`ComponentId`](Component). pub async fn name_by_id(ctx: &DalContext, id: ComponentId) -> ComponentResult<String> { let name_value_id = Self::attribute_value_for_prop(ctx, id, &["root", "si", "name"]).await?; let view_result = AttributeValue::view(ctx, name_value_id).await?; Ok(match view_result { Some(serde_value) => serde_json::from_value(serde_value)?, None => "".into(), }) } /// Returns the name of the [`Component`]. pub async fn name(&self, ctx: &DalContext) -> ComponentResult<String> { Self::name_by_id(ctx, self.id).await } // Returns the resource id from the prop tree pub async fn resource_id(&self, ctx: &DalContext) -> ComponentResult<String> { let prop_path = PropPath::new(["root", "si", "resourceId"]); let prop_id = Prop::find_prop_id_by_path_opt(ctx, self.schema_variant(ctx).await?.id, &prop_path) .await?; if let Some(prop_id) = prop_id { let resource_id_value_id = Self::attribute_value_for_prop_id(ctx, self.id, prop_id).await?; Ok( match AttributeValue::view(ctx, resource_id_value_id).await? { Some(serde_value) => serde_json::from_value(serde_value)?, None => "".into(), }, ) } else { Ok("".into()) } } pub async fn color(&self, ctx: &DalContext) -> ComponentResult<Option<String>> { let color_value_id = Component::attribute_value_for_prop(ctx, self.id(), &["root", "si", "color"]).await?; Ok(match AttributeValue::view(ctx, color_value_id).await? { Some(serde_value) => Some(serde_json::from_value(serde_value)?), None => None, }) } pub async fn color_by_id(ctx: &DalContext, id: ComponentId) -> ComponentResult<Option<String>> { let color_value_id = Component::attribute_value_for_prop(ctx, id, &["root", "si", "color"]).await?; Ok(match AttributeValue::view(ctx, color_value_id).await? { Some(serde_value) => Some(serde_json::from_value(serde_value)?), None => None, }) } #[instrument(level="debug" skip(ctx))] pub async fn get_type_by_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<ComponentType> { let type_value_id = Self::attribute_value_for_prop(ctx, component_id, &["root", "si", "type"]).await?; let type_value = AttributeValue::view(ctx, type_value_id).await?.ok_or( ComponentError::ComponentMissingTypeValueMaterializedView(component_id), )?; Ok(serde_json::from_value(type_value)?) } pub async fn get_type(&self, ctx: &DalContext) -> ComponentResult<ComponentType> { Self::get_type_by_id(ctx, self.id()).await } /// For the given [`ComponentId`], updates the type. If the type is changing from or to an Up/Down Frame, /// this ensures we update the necessary values given the changing data flows pub async fn set_type_by_id( ctx: &DalContext, component_id: ComponentId, new_type: ComponentType, ) -> ComponentResult<()> { // cache the current type let current_type = Self::get_type_by_id(ctx, component_id).await?; // no-op if we're not actually changing the type if new_type == current_type { return Ok(()); } let type_value_id = Self::attribute_value_for_prop(ctx, component_id, &["root", "si", "type"]).await?; let value = serde_json::to_value(new_type)?; AttributeValue::update(ctx, type_value_id, Some(value)).await?; Ok(()) } async fn set_type(&self, ctx: &DalContext, new_type: ComponentType) -> ComponentResult<()> { let type_value_id = Component::attribute_value_for_prop(ctx, self.id(), &["root", "si", "type"]).await?; let value = serde_json::to_value(new_type)?; AttributeValue::update(ctx, type_value_id, Some(value)).await?; Ok(()) } pub async fn root_attribute_value_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<AttributeValueId> { let workspace_snapshot = ctx.workspace_snapshot()?; let mut maybe_root_attribute_value_id = None; for target in workspace_snapshot .outgoing_targets_for_edge_weight_kind(component_id, EdgeWeightKindDiscriminants::Root) .await? { let target_node_weight = workspace_snapshot.get_node_weight(target).await?; if let NodeWeight::AttributeValue(_) = target_node_weight { maybe_root_attribute_value_id = match maybe_root_attribute_value_id { Some(already_found_root_attribute_value_id) => { return Err(ComponentError::MultipleRootAttributeValuesFound( target_node_weight.id().into(), already_found_root_attribute_value_id, component_id, )); } None => Some(target_node_weight.id().into()), }; } } maybe_root_attribute_value_id .ok_or(ComponentError::RootAttributeValueNotFound(component_id)) } pub async fn output_socket_attribute_values( &self, ctx: &DalContext, ) -> ComponentResult<Vec<AttributeValueId>> { ComponentOutputSocket::attribute_values_for_component_id(ctx, self.id()).await } pub async fn input_socket_attribute_values( &self, ctx: &DalContext, ) -> ComponentResult<Vec<AttributeValueId>> { ComponentInputSocket::attribute_values_for_component_id(ctx, self.id()).await } /// Find the attribute values for *this* component and a given prop path pub async fn attribute_values_for_prop( &self, ctx: &DalContext, prop_path: &[&str], ) -> ComponentResult<Vec<AttributeValueId>> { Self::attribute_values_for_prop_by_id(ctx, self.id(), prop_path).await } /// Find the attribute values for a component id and prop path pub async fn attribute_values_for_prop_by_id( ctx: &DalContext, component_id: ComponentId, prop_path: &[&str], ) -> ComponentResult<Vec<AttributeValueId>> { let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; let prop_id = Prop::find_prop_id_by_path(ctx, schema_variant_id, &PropPath::new(prop_path)).await?; Self::attribute_values_for_prop_id(ctx, component_id, prop_id).await } /// Find the attribute values for a component id and prop id pub async fn attribute_values_for_prop_id( ctx: &DalContext, component_id: ComponentId, prop_id: PropId, ) -> ComponentResult<Vec<AttributeValueId>> { let mut result = vec![]; let all_relevant_prop_ids = Prop::all_parent_prop_ids_from_prop_id(ctx, prop_id).await?; let root_attribute_value_id = Component::root_attribute_value_id(ctx, component_id).await?; let mut work_queue = VecDeque::from([root_attribute_value_id]); let mut early_return = false; while let Some(attribute_value_id) = work_queue.pop_front() { let working_prop_id = AttributeValue::prop_id(ctx, attribute_value_id).await?; // We found one! But we might have more. This should ensure we finish everything // at the current rank, but don't descend. if prop_id == working_prop_id { early_return = true; result.push(attribute_value_id); } if !early_return && all_relevant_prop_ids.contains(&working_prop_id) { let children = AttributeValue::get_child_av_ids_in_order(ctx, attribute_value_id).await?; work_queue.extend(children); } } Ok(result) } // Get a single attribute value for this component and a given prop path // Errors if there is no value, or if more than one value exists. pub async fn attribute_value_for_prop_id( ctx: &DalContext, component_id: ComponentId, prop_id: PropId, ) -> ComponentResult<AttributeValueId> { let values = Self::attribute_values_for_prop_id(ctx, component_id, prop_id).await?; if values.len() > 1 { return Err(ComponentError::ComponentHasTooManyValues( component_id, prop_id, )); } match values.first() { Some(value) => Ok(*value), None => Err(ComponentError::ComponentMissingValue(component_id, prop_id)), } } // Get a single attribute value for this component and a given prop path // Errors if there is no value, or if more than one value exists. pub async fn attribute_value_for_prop( ctx: &DalContext, component_id: ComponentId, prop_path: &[&str], ) -> ComponentResult<AttributeValueId> { let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; let prop_id = Prop::find_prop_id_by_path(ctx, schema_variant_id, &PropPath::new(prop_path)).await?; let result = Self::attribute_value_for_prop_id(ctx, component_id, prop_id).await?; Ok(result) } pub async fn domain_prop_attribute_value( &self, ctx: &DalContext, ) -> ComponentResult<AttributeValueId> { Component::attribute_value_for_prop(ctx, self.id(), &["root", "domain"]).await } pub async fn resource_value_prop_attribute_value( &self, ctx: &DalContext, ) -> ComponentResult<AttributeValueId> { Component::attribute_value_for_prop(ctx, self.id(), &["root", "resource_value"]).await } pub async fn attribute_values_for_all_sockets( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Vec<AttributeValueId>> { let mut socket_values: Vec<AttributeValueId> = vec![]; let workspace_snapshot = ctx.workspace_snapshot()?; for socket_target in workspace_snapshot .outgoing_targets_for_edge_weight_kind( component_id, EdgeWeightKindDiscriminants::SocketValue, ) .await? { socket_values.push( workspace_snapshot .get_node_weight(socket_target) .await? .get_attribute_value_node_weight()? .id() .into(), ); } Ok(socket_values) } pub async fn list_av_controlling_func_ids_for_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<HashMap<AttributeValueId, ControllingFuncData>> { let root_av_id: AttributeValueId = Component::root_attribute_value_id(ctx, component_id).await?; let mut av_queue = VecDeque::from([(root_av_id, None)]); let mut result: HashMap<AttributeValueId, ControllingFuncData> = HashMap::new(); while let Some((av_id, maybe_parent_av_id)) = av_queue.pop_front() { let prototype_id = AttributeValue::prototype_id(ctx, av_id).await?; let func_id = AttributePrototype::func_id(ctx, prototype_id).await?; let this_tuple = ControllingFuncData { func_id, av_id, is_dynamic_func: Func::is_dynamic(ctx, func_id).await?, }; // if av has a parent and parent is controlled by dynamic func, that's the controller // else av controls itself let controlling_tuple = if let Some(parent_av_id) = maybe_parent_av_id { let parent_controlling_data = *result.get(&parent_av_id).ok_or( ComponentError::MissingControllingFuncDataForParentAttributeValue(parent_av_id), )?; if parent_controlling_data.is_dynamic_func { parent_controlling_data } else { this_tuple } } else { this_tuple }; // { // let prop_id = AttributeValue::prop_id_for_id(ctx, av_id).await?; // let this_prop = Prop::get_by_id(ctx, prop_id).await?; // // let controlling_prop = { // let prop_id = // AttributeValue::prop_id_for_id(ctx, controlling_tuple.av_id).await?; // Prop::get_by_id(ctx, prop_id).await? // }; // let controlling_func = Func::get_by_id(ctx, controlling_tuple.func_id).await?; // // let controlled_by_ancestor = controlling_tuple.av_id != this_tuple.av_id; // println!("==========================="); // // println!( // "Prop {} is controlled by {}, through func {}({}dynamic){}", // this_prop.name, // if controlled_by_ancestor { // controlling_prop.name // } else { // "itself".to_string() // }, // controlling_func.name, // if controlling_tuple.is_dynamic_func { // "" // } else { // "non-" // }, // if controlled_by_ancestor { // format!( // " - controlled. original func {}({}dynamic)", // func.name, // if this_tuple.is_dynamic_func { // "" // } else { // "non-" // } // ) // } else { // "".to_string() // } // ); // } result.insert(av_id, controlling_tuple); av_queue.append( &mut AttributeValue::get_child_av_ids_in_order(ctx, av_id) .await? .into_iter() .map(|child_av_id| (child_av_id, Some(av_id))) .collect::<VecDeque<_>>(), ); } Ok(result) } /// Checks the destination and source component to determine if data flow between them /// Both "deleted" and not deleted Components can feed data into /// "deleted" Components. **ONLY** not deleted Components can feed /// data into not deleted Components. pub async fn should_data_flow_between_components( ctx: &DalContext, destination_component_id: ComponentId, source_component_id: ComponentId, ) -> ComponentResult<bool> { let destination_component_is_delete = Self::is_set_to_delete(ctx, destination_component_id).await?; let source_component_is_delete = Self::is_set_to_delete(ctx, source_component_id).await?; Ok( match (destination_component_is_delete, source_component_is_delete) { (None, _) | (_, None) => false, (Some(destination_component_is_delete), Some(source_component_is_delete)) => { destination_component_is_delete || !source_component_is_delete } }, ) } /// Returns the value of the "to_delete" field using solely the graph node weight. pub async fn is_set_to_delete( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Option<bool>> { match ctx .workspace_snapshot()? .get_node_weight_opt(component_id) .await { Some(component_node_weight) => Ok(Some( component_node_weight .get_component_node_weight()? .to_delete(), )), None => Ok(None), } } async fn modify<L>(self, ctx: &DalContext, lambda: L) -> ComponentResult<Self> where L: FnOnce(&mut Self) -> ComponentResult<()>, { let original_component = self.clone(); let mut component = self; let before = ComponentContentV2::from(component.clone()); lambda(&mut component)?; // The `to_delete` lives on the node itself, not in the content, so we need to be a little // more manual when updating that field. if component.to_delete != original_component.to_delete { let component_node_weight = ctx .workspace_snapshot()? .get_node_weight(original_component.id) .await? .get_component_node_weight()?; let mut new_component_node_weight = component_node_weight.clone(); new_component_node_weight.set_to_delete(component.to_delete); ctx.workspace_snapshot()? .add_or_replace_node(NodeWeight::Component(new_component_node_weight)) .await?; } let updated = ComponentContentV2::from(component.clone()); if updated != before { let (hash, _) = ctx.layer_db().cas().write( Arc::new(ComponentContent::V2(updated.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; ctx.workspace_snapshot()? .update_content(component.id.into(), hash) .await?; } let component_node_weight = ctx .workspace_snapshot()? .get_node_weight(original_component.id) .await? .get_component_node_weight()?; Ok(Component::assemble(&component_node_weight, updated)) } /// Remove a [Component] from the graph, and all related nodes #[instrument(level = "info", skip(ctx))] pub async fn remove(ctx: &DalContext, id: ComponentId) -> ComponentResult<()> { let root_attribute_value_id = Self::root_attribute_value_id(ctx, id).await?; // Remove all geometries for the component Geometry::remove_all_for_component_id(ctx, id).await?; // Remove all actions for this component from queue Action::remove_all_for_component_id(ctx, id).await?; WsEvent::action_list_updated(ctx) .await? .publish_on_commit(ctx) .await?; // Deleting the root attribute value will remove all ValueSubscription edges that point to it. AttributeValue::remove(ctx, root_attribute_value_id).await?; // Remove the component itself ctx.workspace_snapshot()?.remove_node_by_id(id).await?; Ok(()) } /// A [`Component`] is allowed to be removed from the graph if it meets the following /// requirements: /// /// 1. It doesn't have a populated resource. /// 2. It is not feeding data to a [`Component`] that has a populated resource. /// 3. It doesn't have descendants with resources #[instrument(level = "debug", skip_all)] pub async fn allowed_to_be_removed(&self, ctx: &DalContext) -> ComponentResult<bool> { if self.resource(ctx).await?.is_some() { debug!( "component {:?} cannot be removed because it has a resource", self.id ); return Ok(false); } // Check subscribers too let subscribers = Self::subscribers(ctx, self.id()).await?; for (_, subscriber_apa_id) in subscribers { let subscriber_ap_id = AttributePrototypeArgument::prototype_id(ctx, subscriber_apa_id).await?; let Some(subscriber_av_id) = AttributePrototype::attribute_value_id(ctx, subscriber_ap_id).await? else { continue; }; let connected_to_component_id = AttributeValue::component_id(ctx, subscriber_av_id).await?; if Self::resource_by_id(ctx, connected_to_component_id) .await? .is_some() { debug!( "component {:?} cannot be removed because {:?} has resource", self.id, connected_to_component_id ); return Ok(false); } } debug!("component {:?} can be removed", self.id,); Ok(true) } pub async fn delete(self, ctx: &DalContext) -> ComponentResult<Option<Self>> { ctx.workspace_snapshot()? .prop_suggestions_cache_no_populate() .await? .remove_component(self.id())?; if self.allowed_to_be_removed(ctx).await? { Self::remove(ctx, self.id).await?; Ok(None) } else { Ok(Some(self.set_to_delete(ctx, true).await?)) } } pub async fn set_to_delete(self, ctx: &DalContext, to_delete: bool) -> ComponentResult<Self> { let component_id = self.id; let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; let original_to_delete = self.to_delete; let modified = self .modify(ctx, |component| { component.to_delete = to_delete; Ok(()) }) .await?; // If we're clearing the `to_delete` flag, we need to make sure our inputs are updated // appropriately, as we may have an input connected to a still `to_delete` component, and // we should not be using it for input as long as it's still marked `to_delete`. // // If we're setting the `to_delete` flag, then we may need to pick up inputs from other // `to_delete` Components that we were ignoring before. // // This will update more than is strictly necessary, but it will ensure that everything is // correct. let input_av_ids: Vec<AttributeValueId> = modified.input_socket_attribute_values(ctx).await?; ctx.add_dependent_values_and_enqueue(input_av_ids).await?; // Deal with deletion actions, but only if we're transitioning from not being to_delete // into being to_delete. if to_delete && !original_to_delete { // Enqueue delete actions for component for prototype in ActionPrototype::find_by_kind_for_schema_or_variant( ctx, ActionKind::Destroy, schema_variant_id, ) .await? { Action::new(ctx, prototype.id(), Some(component_id)).await?; } } else if !to_delete { // Remove delete actions for component Action::remove_all_for_component_id(ctx, component_id).await?; WsEvent::action_list_updated(ctx) .await? .publish_on_commit(ctx) .await?; } Ok(modified) } /// If the attribute value is somewhere in 'root/domain', the component has a resource, and a single update function, /// and there isn't any action already enqueued for this component, enqueue it! pub async fn enqueue_update_action_if_applicable( ctx: &DalContext, modified_av_id: AttributeValueId, ) -> ComponentResult<Option<Action>> { if let Some(prop_id) = AttributeValue::prop_id_opt(ctx, modified_av_id).await? { if Prop::path_by_id(ctx, prop_id) .await? .is_descendant_of(&PropPath::new(["root", "domain"])) { let component_id = AttributeValue::component_id(ctx, modified_av_id).await?; if Component::resource_by_id(ctx, component_id) .await? .is_some() { // then if the current component has an update action, enqueue it let schema_variant_id = Component::schema_variant_id(ctx, component_id).await?; let mut prototypes_for_variant = ActionPrototype::find_by_kind_for_schema_or_variant( ctx, ActionKind::Update, schema_variant_id, ) .await?; if prototypes_for_variant.len() > 1 { // if there are multiple update funcs, not sure which one to enqueue! return Ok(None); } if let Some(prototype) = prototypes_for_variant.pop() { // Don't enqueue an update if there is already an Action of any kind enqueued for this Component. if Action::find_for_component_id(ctx, component_id) .await? .is_empty() { let new_action = Action::new(ctx, prototype.id(), Some(component_id)).await?; return Ok(Some(new_action)); } } } } } Ok(None) } pub async fn duplicate_without_connections( &self, ctx: &DalContext, view_id: ViewId, component_geometry: RawGeometry, name_prefix: Option<&str>, ) -> ComponentResult<Self> { let schema_variant = self.schema_variant(ctx).await?; let name = if let Some(name_prefix) = name_prefix { format!("{name_prefix}{0}", self.name(ctx).await?) } else { Self::generate_copy_name(self.name(ctx).await?) }; let mut pasted_comp = Component::new(ctx, name.clone(), schema_variant.id(), view_id).await?; pasted_comp .set_geometry( ctx, view_id, component_geometry.x, component_geometry.y, component_geometry.width, component_geometry.height, ) .await?; pasted_comp.clone_attributes_from(ctx, self.id()).await?; // need to update the name AFTER we clone attributes, because the name IS an attribute pasted_comp.set_name(ctx, &name).await?; Ok(pasted_comp) } /// For a given set of components, duplicate them in the provided view /// The duplicate function behaves differently than bulk_copy: /// - Socket connections (Component::connect) are ignored - duplicated components won't have these /// - Subscriptions (value::subscribe) are preserved - both external and internal subscriptions are maintained /// - Management connections are preserved between duplicated components, and dropped otherwise pub async fn duplicate( ctx: &mut DalContext, to_view_id: ViewId, components: Vec<ComponentId>, name_prefix: &str, ) -> ComponentResult<Vec<ComponentId>> { let mut pasted_component_ids = vec![]; let mut to_pasted_id = HashMap::new(); for component_id in components.into_iter() { let component = Component::get_by_id(ctx, component_id).await?; let pasted_component = component .duplicate_without_connections( ctx, to_view_id, RawGeometry { x: 0, y: 0, width: None, height: None, }, Some(name_prefix), ) .await?; pasted_component_ids.push(pasted_component.id()); to_pasted_id.insert(component_id, pasted_component.id()); } ctx.workspace_snapshot()?.cleanup().await?; // Copy correct connections (prop to prop and management) for (&og_component_id, &pasted_component_id) in &to_pasted_id { // Copy manager connections for manager_id in Component::managers_by_id(ctx, og_component_id).await? { // If we were managed by a component that was also pasted, we should be managed by // the pasted version--otherwise we're unmanaged! if let Some(&pasted_manager_id) = to_pasted_id.get(&manager_id) { Component::manage_component(ctx, pasted_manager_id, pasted_component_id) .await?; } } // Find duplicated components that subscribe to copied components, and // resubscribe them to the pasted component for (path, maybe_pasted_subscriber_apa_id) in Component::subscribers(ctx, og_component_id).await? { let maybe_pasted_subscriber_ap_id = AttributePrototypeArgument::prototype_id(ctx, maybe_pasted_subscriber_apa_id) .await?; let Some(maybe_pasted_subscriber_av_id) = AttributePrototype::attribute_value_id(ctx, maybe_pasted_subscriber_ap_id) .await? else { continue; }; let maybe_pasted_subscriber_id = AttributeValue::component_id(ctx, maybe_pasted_subscriber_av_id).await?; if pasted_component_ids.contains(&maybe_pasted_subscriber_id) { // now we know that the subcriber we're dealing with is one that was pasted! AttributePrototypeArgument::set_value_source( ctx, maybe_pasted_subscriber_apa_id, ValueSubscription::new(ctx, pasted_component_id, path).await?, ) .await?; } } } Ok(pasted_component_ids) } // Copy a batch of components, and replicate connections between them pub async fn batch_copy( ctx: &mut DalContext, to_view_id: ViewId, components: Vec<(ComponentId, RawGeometry)>, ) -> ComponentResult<Vec<ComponentId>> { // Paste all the components and get the mapping from original to pasted let mut pasted_component_ids = vec![]; let mut to_pasted_id = HashMap::new(); for (component_id, raw_geometry) in components.into_iter() { let component = Component::get_by_id(ctx, component_id).await?; let pasted_component = component .duplicate_without_connections(ctx, to_view_id, raw_geometry, None) .await?; pasted_component_ids.push(pasted_component.id()); to_pasted_id.insert(component_id, pasted_component.id()); } let maybe_pasted = |id: ComponentId| to_pasted_id.get(&id).copied().unwrap_or(id); // Fix parentage and connections for (&component_id, &pasted_component_id) in &to_pasted_id { // Copy manager connections for manager_id in Component::managers_by_id(ctx, component_id).await? { // If we were managed by a component that was also pasted, we should be managed by // the pasted version--otherwise we're still managed by the original Component::manage_component( ctx, maybe_pasted(manager_id), maybe_pasted(component_id), ) .await?; } // Find pasted components that subscribe to copied components, and // resubscribe them to the pasted component for (path, subscriber_apa_id) in Component::subscribers(ctx, component_id).await? { let subscriber_ap_id = AttributePrototypeArgument::prototype_id(ctx, subscriber_apa_id).await?; let Some(subscriber_av_id) = AttributePrototype::attribute_value_id(ctx, subscriber_ap_id).await? else { continue; }; let subscriber_id = AttributeValue::component_id(ctx, subscriber_av_id).await?; if pasted_component_ids.contains(&subscriber_id) { AttributePrototypeArgument::set_value_source( ctx, subscriber_apa_id, ValueSubscription::new(ctx, pasted_component_id, path).await?, ) .await?; } } } Ok(pasted_component_ids) } pub async fn add_to_view( ctx: &DalContext, component_id: ComponentId, view_id: ViewId, raw_geometry: RawGeometry, ) -> ComponentResult<()> { if Geometry::try_get_by_component_and_view(ctx, component_id, view_id) .await? .is_some() { return Err(ComponentError::ComponentAlreadyInView( component_id, view_id, )); } let mut geometry = Geometry::new_for_component(ctx, component_id, view_id).await?; geometry.update(ctx, raw_geometry).await?; Ok(()) } #[instrument(level = "debug", skip(ctx))] pub async fn upgrade_to_new_variant( ctx: &DalContext, original_component_id: ComponentId, schema_variant_id: SchemaVariantId, ) -> ComponentResult<Component> { let original_component = Self::get_by_id(ctx, original_component_id).await?; // ================================================================================ // Cache original component data // ================================================================================ let snap = ctx.workspace_snapshot()?; let original_component_node_weight = snap.get_node_weight(original_component.id).await?; let original_component_name = Self::name_by_id(ctx, original_component_id).await?; let original_component_lineage_id = original_component_node_weight.lineage_id(); let original_managed = original_component.get_managed(ctx).await?; let original_managers = original_component.managers(ctx).await?; let original_root_id = Component::root_attribute_value_id(ctx, original_component_id).await?; let original_subscriber_apas = AttributeValue::subscribers(ctx, original_root_id).await?; // Save which attribute values are marked as default subscription sources let mut original_default_sub_source_paths = vec![]; let mut av_work_queue = VecDeque::from([original_root_id]); while let Some(av_id) = av_work_queue.pop_front() { if AttributeValue::is_default_subscription_source(ctx, av_id).await? { if let Some(path) = AttributeValue::get_path_for_id(ctx, av_id).await? { original_default_sub_source_paths.push(path); } } av_work_queue.extend(AttributeValue::child_av_ids(ctx, av_id).await?); } let geometry_ids = Geometry::list_ids_by_component(ctx, original_component_id).await?; // ================================================================================ // Create new component and run changes that depend on the old one still existing // ================================================================================ let new_component_with_temp_id = Component::new_with_content_address_and_no_geometry_no_default_subscriptions( ctx, original_component_name.clone(), schema_variant_id, original_component_node_weight.content_hash(), ) .await?; // Move geometries to new component for geometry_id in geometry_ids { snap.remove_edge( geometry_id, original_component_id, EdgeWeightKindDiscriminants::Represents, ) .await?; snap.add_edge( geometry_id, EdgeWeight::new(EdgeWeightKind::Represents), new_component_with_temp_id.id, ) .await?; } let new_schema_variant_id = new_component_with_temp_id.schema_variant(ctx).await?.id(); if new_schema_variant_id != schema_variant_id { return Err(ComponentError::ComponentIncorrectSchemaVariant( new_component_with_temp_id.id(), )); } new_component_with_temp_id .merge_from_component_with_different_schema_variant(ctx, original_component.id()) .await?; if schema_variant_id != Component::get_by_id(ctx, new_component_with_temp_id.id()) .await? .schema_variant(ctx) .await? .id() { return Err(ComponentError::ComponentIncorrectSchemaVariant( new_component_with_temp_id.id(), )); } // Remove old component connections for &original_managed_id in &original_managed { Component::unmanage_component(ctx, original_component_id, original_managed_id).await?; } for &original_manager_id in &original_managers { Component::unmanage_component(ctx, original_manager_id, original_component_id).await?; } let mut original_subscriber_prototypes = vec![]; for (path, apa_id) in original_subscriber_apas { let prototype_id = AttributePrototypeArgument::prototype_id(ctx, apa_id).await?; AttributePrototypeArgument::remove(ctx, apa_id).await?; original_subscriber_prototypes.push((path, prototype_id)); } // Let's requeue any Actions for the component Self::requeue_actions_for_upgraded_component( ctx, original_component.id(), new_component_with_temp_id.id(), new_schema_variant_id, ) .await?; // ======================================== // Delete original component // ======================================== // Remove the original resource so that we don't queue a delete action original_component.clear_resource(ctx).await?; Self::remove(ctx, original_component.id).await?; snap.cleanup().await?; // ======================================== // Finish up the new component // ======================================== // Now we replace the new component id with the id of the original one snap.update_node_id( new_component_with_temp_id.id, original_component_id, original_component_lineage_id, ) .await?; // Re fetch the component with the old id let upgraded_component = Self::get_by_id(ctx, original_component_id).await?; let mut diagram_sockets = HashMap::new(); let payload = upgraded_component .into_frontend_type(ctx, None, ChangeStatus::Unmodified, &mut diagram_sockets) .await?; WsEvent::component_upgraded(ctx, payload, upgraded_component.id()) .await? .publish_on_commit(ctx) .await?; // Restore connections on new component for original_managed_id in original_managed { Component::manage_component(ctx, upgraded_component.id(), original_managed_id).await?; } for original_manager_id in original_managers { Component::manage_component(ctx, original_manager_id, upgraded_component.id()).await?; } // Reconnect subscribers let finalized_root_id = Component::root_attribute_value_id(ctx, upgraded_component.id()).await?; for (path, prototype_id) in original_subscriber_prototypes { AttributePrototype::add_arg_to_intrinsic( ctx, prototype_id, ValueSubscription { attribute_value_id: finalized_root_id, path, }, ) .await?; } // Restore default subscription source markings for path in original_default_sub_source_paths { // get_path_for_id returns paths like "root/domain/prop" // but we're resolving from finalized_root_id which is already the root, // so we need to strip the "root/" prefix and convert to JSON pointer format let path_without_root = path.strip_prefix("root/").unwrap_or(&path); let json_pointer_path = format!("/{path_without_root}"); let attribute_path = AttributePath::from_json_pointer(json_pointer_path); if let Some(av_id) = attribute_path.resolve(ctx, finalized_root_id).await? { AttributeValue::set_as_default_subscription_source(ctx, av_id).await?; } } ctx.workspace_snapshot()? .clear_prop_suggestions_cache() .await; Ok(upgraded_component) } async fn requeue_actions_for_upgraded_component( ctx: &DalContext, old_component_id: ComponentId, new_component_id: ComponentId, new_schema_variant_id: SchemaVariantId, ) -> ComponentResult<()> { // Remove any actions created for the new component as a side effect of the upgrade // Then loop through the existing queued actions for the old component and re-add them piecemeal. Action::remove_all_for_component_id(ctx, new_component_id).await?; let queued_for_old_component = Action::find_for_component_id(ctx, old_component_id).await?; let available_for_new_component = ActionPrototype::list_for_schema_and_variant_id(ctx, new_schema_variant_id).await?; for existing_queued in queued_for_old_component { let action = Action::get_by_id(ctx, existing_queued).await?; let action_prototype_id = Action::prototype_id(ctx, existing_queued).await?; let existing_queued_prototype = ActionPrototype::get_by_id(ctx, action_prototype_id).await?; // what do we do about the various states? // maybe you shouldn't upgrade a component if an action // is dispatched or running for the current? match action.state() { ActionState::Failed | ActionState::OnHold | ActionState::Queued => { let func_id = ActionPrototype::func_id(ctx, action_prototype_id).await?; let queued_func = Func::get_by_id(ctx, func_id).await?; for available_action_prototype in available_for_new_component.clone() { let available_func_id = ActionPrototype::func_id(ctx, available_action_prototype.id()).await?; let available_func = Func::get_by_id(ctx, available_func_id).await?; // check if the create/update/refresh/delete action // should be replaced by an overlay // otherwise enqueue the same func with the same name and kind if (available_func.kind == queued_func.kind && existing_queued_prototype.kind == available_action_prototype.kind && existing_queued_prototype.kind != ActionKind::Manual) || (available_func.name == queued_func.name && available_func.kind == queued_func.kind) { Action::new( ctx, available_action_prototype.id(), Some(new_component_id), ) .await?; } } } ActionState::Running | ActionState::Dispatched => continue, } } Ok(()) } fn generate_copy_name(name: String) -> String { if name.ends_with("- Copy") { name } else { format!("{name} - Copy") } } /// This method finds the [`AttributeValueId`](crate::AttributeValue) corresponding to either "/root/code" or /// "/root/qualification" for the given [`ComponentId`](Component) and ['LeafKind'](LeafKind). pub async fn find_map_attribute_value_for_leaf_kind( ctx: &DalContext, component_id: ComponentId, leaf_kind: LeafKind, ) -> ComponentResult<AttributeValueId> { let attribute_value_id = match leaf_kind { LeafKind::CodeGeneration => { Component::find_code_map_attribute_value_id(ctx, component_id).await? } LeafKind::Qualification => { Component::find_qualification_map_attribute_value_id(ctx, component_id).await? } }; Ok(attribute_value_id) } pub async fn restore_from_base_change_set( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<()> { let base_change_set_ctx = ctx.clone_with_base().await?; ctx.workspace_snapshot()? .import_component_subgraph(&base_change_set_ctx.workspace_snapshot()?, component_id) .await?; let component = Component::get_by_id(ctx, component_id).await?; ctx.add_dependent_values_and_enqueue(component.input_socket_attribute_values(ctx).await?) .await?; Geometry::restore_all_for_component_id(ctx, component_id).await?; Ok(()) } pub async fn exists_on_head_by_ids( ctx: &DalContext, component_ids: &[ComponentId], ) -> ComponentResult<HashSet<ComponentId>> { let snapshot = { let head_ctx = ctx.clone_with_head().await?; head_ctx.workspace_snapshot()? }; let mut component_ids_on_head = HashSet::new(); for component_id in component_ids.iter().copied() { if snapshot.get_node_weight_opt(component_id).await.is_some() { component_ids_on_head.insert(component_id); } } Ok(component_ids_on_head) } pub async fn can_be_upgraded_by_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<bool> { let schema_variant = Component::schema_variant_for_component_id(ctx, component_id).await?; let schema_id = Component::schema_id_for_component_id(ctx, component_id).await?; let default_schema_variant_id = SchemaVariant::default_id_for_schema(ctx, schema_id).await?; let newest_schema_variant_id = match SchemaVariant::get_unlocked_for_schema(ctx, schema_id).await? { Some(unlocked_schema_variant) => unlocked_schema_variant.id(), None => default_schema_variant_id, }; Ok(if newest_schema_variant_id != schema_variant.id() { // There's a chance that the exact same asset was installed in // different change sets and then applied to head. In that case, // there's no need to show the upgrade for this component, since the // upgrade will be effectively a no-op. let current_module = Module::find_for_member_id(ctx, schema_variant.id()).await?; let new_module = Module::find_for_member_id(ctx, newest_schema_variant_id).await?; match (current_module, new_module) { (Some(current_module), Some(new_module)) => { current_module.root_hash() != new_module.root_hash() } _ => true, } } else { false }) } /// Is there a newer version of the schema variant that this component is using? pub async fn can_be_upgraded(&self, ctx: &DalContext) -> ComponentResult<bool> { let schema_variant = self.schema_variant(ctx).await?; let schema = self.schema(ctx).await?; let default_schema_variant_id = SchemaVariant::default_id_for_schema(ctx, schema.id()).await?; let newest_schema_variant_id = match SchemaVariant::get_unlocked_for_schema(ctx, schema.id()).await? { Some(unlocked_schema_variant) => unlocked_schema_variant.id(), None => default_schema_variant_id, }; Ok(if newest_schema_variant_id != schema_variant.id() { // There's a chance that the exact same asset was installed in // different change sets and then applied to head. In that case, // there's no need to show the upgrade for this component, since the // upgrade will be effectively a no-op. let current_module = Module::find_for_member_id(ctx, schema_variant.id()).await?; let new_module = Module::find_for_member_id(ctx, newest_schema_variant_id).await?; match (current_module, new_module) { (Some(current_module), Some(new_module)) => { current_module.root_hash() != new_module.root_hash() } _ => true, } } else { false }) } /// Remove a [`Manages`](`crate::edge_weight::EdgeWeightKind::Manages`) /// edge from a manager component to a managed component pub async fn unmanage_component( ctx: &DalContext, manager_component_id: ComponentId, managed_component_id: ComponentId, ) -> ComponentResult<()> { ctx.workspace_snapshot()? .remove_edge( manager_component_id, managed_component_id, EdgeWeightKindDiscriminants::Manages, ) .await?; Ok(()) } /// Add a [`Manages`](`crate::edge_weight::EdgeWeightKind::Manages`) edge /// from a manager component to a managed component, if the managed /// component is based on a managed schema pub async fn manage_component( ctx: &DalContext, manager_component_id: ComponentId, managed_component_id: ComponentId, ) -> ComponentResult<SummaryDiagramManagementEdge> { let manager_schema_id = Component::schema_for_component_id(ctx, manager_component_id) .await? .id(); let managed_component_schema_id = Self::schema_for_component_id(ctx, managed_component_id) .await? .id(); let guard = ctx.workspace_snapshot()?.enable_cycle_check().await; Component::add_manages_edge_to_component( ctx, manager_component_id, managed_component_id, EdgeWeightKind::Manages, ) .await?; drop(guard); Ok(SummaryDiagramManagementEdge::new( manager_schema_id, managed_component_schema_id, manager_component_id, managed_component_id, )) } /// Return the IDs of all the [`Components`](Component) that manage this [`Component`](Component). pub async fn managers(&self, ctx: &DalContext) -> ComponentResult<Vec<ComponentId>> { Self::managers_by_id(ctx, self.id).await } /// Return the IDs of all the [`Components`](Component) that manage the [`Component`](Component) corresponding /// to the provided ID. pub async fn managers_by_id( ctx: &DalContext, id: ComponentId, ) -> ComponentResult<Vec<ComponentId>> { let mut result = Vec::new(); let snapshot = ctx.workspace_snapshot()?; for source_idx in snapshot .incoming_sources_for_edge_weight_kind(id, EdgeWeightKindDiscriminants::Manages) .await? { let node_weight = snapshot.get_node_weight(source_idx).await?; if let NodeWeight::Component(_) = &node_weight { result.push(node_weight.id().into()); } } Ok(result) } /// Return the ids of all the components managed by this component pub async fn get_managed(&self, ctx: &DalContext) -> ComponentResult<Vec<ComponentId>> { Self::get_managed_by_id(ctx, self.id()).await } /// Return the ids of all the components managed by this component pub async fn get_managed_by_id( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<Vec<ComponentId>> { let mut result = vec![]; let snapshot = ctx.workspace_snapshot()?; for target_idx in snapshot .outgoing_targets_for_edge_weight_kind( component_id, EdgeWeightKindDiscriminants::Manages, ) .await? { let node_weight = snapshot.get_node_weight(target_idx).await?; if let NodeWeight::Component(_) = &node_weight { result.push(node_weight.id().into()); } } Ok(result) } pub async fn into_frontend_type( &self, ctx: &DalContext, maybe_geometry: Option<&Geometry>, change_status: ChangeStatus, diagram_sockets: &mut HashMap<SchemaVariantId, Vec<DiagramSocket>>, ) -> ComponentResult<DiagramComponentView> { let schema_variant = self.schema_variant(ctx).await?; let schema_sockets = match diagram_sockets.entry(schema_variant.id()) { hash_map::Entry::Vacant(entry) => { let (output_sockets, input_sockets) = SchemaVariant::list_all_sockets(ctx, schema_variant.id()).await?; let (management_input_socket, management_output_socket) = SchemaVariant::get_management_sockets(ctx, schema_variant.id()).await?; let mut sockets = vec![]; sockets.push(management_input_socket); for socket in input_sockets { sockets.push(DiagramSocket { id: socket.id().to_string(), label: socket.name().to_string(), connection_annotations: socket .connection_annotations() .into_iter() .map(|a| a.into()) .collect(), direction: DiagramSocketDirection::Input, max_connections: match socket.arity() { SocketArity::Many => None, SocketArity::One => Some(1), }, is_required: Some(false), node_side: DiagramSocketNodeSide::Left, is_management: Some(false), value: None, }); } if let Some(management_output_socket) = management_output_socket { sockets.push(management_output_socket); } for socket in output_sockets { sockets.push(DiagramSocket { id: socket.id().to_string(), label: socket.name().to_string(), connection_annotations: socket .connection_annotations() .into_iter() .map(|a| a.into()) .collect(), direction: DiagramSocketDirection::Output, max_connections: match socket.arity() { SocketArity::Many => None, SocketArity::One => Some(1), }, is_required: Some(false), node_side: DiagramSocketNodeSide::Right, is_management: Some(false), value: None, }); } entry.insert(sockets.to_owned()); sockets } hash_map::Entry::Occupied(entry) => entry.get().to_owned(), }; let mut sockets = Vec::new(); for mut comp_socket in schema_sockets.clone() { if let Some(is_managed) = comp_socket.is_management { // management sockets do not have values, so don't try to get them // but we still want to return them, silly silly if is_managed { sockets.push(comp_socket.clone()); continue; } } let socket_value = match comp_socket.direction { DiagramSocketDirection::Bidirectional => None, DiagramSocketDirection::Input => { ComponentInputSocket::value_for_input_socket_id_for_component_id_opt( ctx, self.id(), InputSocketId::from_str(&comp_socket.id)?, ) .await? } DiagramSocketDirection::Output => { ComponentOutputSocket::value_for_output_socket_id_for_component_id_opt( ctx, self.id(), OutputSocketId::from_str(&comp_socket.id)?, ) .await? } }; comp_socket.value = socket_value; sockets.push(comp_socket); } let schema = SchemaVariant::schema_for_schema_variant_id(ctx, schema_variant.id()).await?; let schema_id = schema.id(); let updated_info = { let history_actor = ctx.history_actor(); let actor = ActorView::from_history_actor(ctx, *history_actor).await?; serde_json::to_value(HistoryEventMetadata { actor, timestamp: self.timestamp().updated_at, })? }; let created_info = { let history_actor = ctx.history_actor(); let actor = ActorView::from_history_actor(ctx, *history_actor).await?; serde_json::to_value(HistoryEventMetadata { actor, timestamp: self.timestamp().created_at, })? }; let can_be_upgraded = self.can_be_upgraded(ctx).await?; let geometry = if let Some(geometry) = maybe_geometry { let view_id = Geometry::get_view_id_by_id(ctx, geometry.id()).await?; Some(GeometryAndView { view_id, geometry: geometry.into_raw(), }) } else { None }; Ok(DiagramComponentView { id: self.id(), component_id: self.id(), schema_name: schema.name().to_owned(), schema_id, schema_docs_link: schema_variant.link(), schema_variant_id: schema_variant.id(), schema_variant_name: schema_variant.version().to_owned(), schema_category: schema_variant.category().to_owned(), display_name: self.name(ctx).await?, resource_id: self.resource_id(ctx).await?, component_type: self.get_type(ctx).await?.to_string(), color: self.color(ctx).await?.unwrap_or("#111111".into()), change_status: change_status.into(), has_resource: self.resource(ctx).await?.is_some(), sockets, updated_info, created_info, deleted_info: serde_json::Value::Null, to_delete: self.to_delete(), can_be_upgraded, from_base_change_set: false, view_data: geometry, }) } pub async fn into_frontend_type_for_default_view( &self, ctx: &DalContext, change_status: ChangeStatus, diagram_sockets: &mut HashMap<SchemaVariantId, Vec<DiagramSocket>>, ) -> ComponentResult<DiagramComponentView> { let default_view_id = View::get_id_for_default(ctx).await?; let geometry = self.geometry(ctx, default_view_id).await?; self.into_frontend_type(ctx, Some(&geometry), change_status, diagram_sockets) .await } /// Get a short, human-readable title suitable for debugging/display. pub async fn fmt_title(ctx: &DalContext, component_id: ComponentId) -> String { Self::fmt_title_fallible(ctx, component_id) .await .unwrap_or_else(|e| e.to_string()) } async fn fmt_title_fallible( ctx: &DalContext, component_id: ComponentId, ) -> ComponentResult<String> { let schema_variant_id = Self::schema_variant_id(ctx, component_id).await?; Ok(format!( "{} {} ({})", SchemaVariant::fmt_title(ctx, schema_variant_id).await, Self::name_by_id(ctx, component_id).await?, component_id )) } } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentCreatedPayload { pub component: DiagramComponentView, change_set_id: ChangeSetId, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentUpdatedPayload { pub component: DiagramComponentView, pub change_set_id: ChangeSetId, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentUpgradedPayload { component: DiagramComponentView, change_set_id: ChangeSetId, original_component_id: ComponentId, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentDeletedPayload { component_id: ComponentId, change_set_id: ChangeSetId, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase", tag = "type")] pub enum ConnectionDeletedPayload { #[serde(rename_all = "camelCase")] ManagementEdge { from_component_id: ComponentId, to_component_id: ComponentId, }, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, derive_more::From)] #[serde(rename_all = "camelCase", tag = "type")] pub enum ConnectionUpsertedPayload { ManagementEdge(SummaryDiagramManagementEdge), } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentPosition { x: isize, y: isize, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentSize { width: Option<isize>, height: Option<isize>, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentSetPosition { component_id: ComponentId, x: isize, y: isize, width: Option<isize>, height: Option<isize>, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ComponentSetPositionPayload { change_set_id: ChangeSetId, view_id: ViewId, positions: Vec<ComponentSetPosition>, // Used so the client can ignore the messages it caused. created by the frontend, and not stored client_ulid: Option<ulid::Ulid>, } impl ComponentSetPositionPayload { pub fn change_set_id(&self) -> ChangeSetId { self.change_set_id } } impl WsEvent { pub async fn reflect_component_position( workspace_pk: WorkspacePk, change_set_id: ChangeSetId, payload: ComponentSetPositionPayload, ) -> WsEventResult<Self> { WsEvent::new_raw( workspace_pk, Some(change_set_id), None, None, WsPayload::SetComponentPosition(payload), ) .await } pub async fn set_component_position( ctx: &DalContext, change_set_id: ChangeSetId, view_id: ViewId, geometries: Vec<(Ulid, RawGeometry)>, client_ulid: Option<ulid::Ulid>, ) -> WsEventResult<Self> { let mut positions: Vec<ComponentSetPosition> = vec![]; for (component_id, geometry) in geometries { positions.push(ComponentSetPosition { component_id: component_id.into(), x: geometry.x, y: geometry.y, width: geometry.width, height: geometry.height, }); } WsEvent::new( ctx, WsPayload::SetComponentPosition(ComponentSetPositionPayload { change_set_id, view_id, positions, client_ulid, }), ) .await } pub async fn component_created( ctx: &DalContext, component: DiagramComponentView, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::ComponentCreated(ComponentCreatedPayload { change_set_id: ctx.change_set_id(), component, }), ) .await } pub async fn connection_upserted( ctx: &DalContext, payload: ConnectionUpsertedPayload, ) -> WsEventResult<Self> { WsEvent::new(ctx, WsPayload::ConnectionUpserted(payload)).await } pub async fn manages_edge_deleted( ctx: &DalContext, from_component_id: ComponentId, to_component_id: ComponentId, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::ConnectionDeleted(ConnectionDeletedPayload::ManagementEdge { from_component_id, to_component_id, }), ) .await } pub async fn component_updated( ctx: &DalContext, payload: DiagramComponentView, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::ComponentUpdated(ComponentUpdatedPayload { component: payload, change_set_id: ctx.change_set_id(), }), ) .await } pub async fn component_upgraded( ctx: &DalContext, payload: DiagramComponentView, original_component_id: ComponentId, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::ComponentUpgraded(ComponentUpgradedPayload { component: payload, change_set_id: ctx.change_set_id(), original_component_id, }), ) .await } pub async fn component_deleted( ctx: &DalContext, component_id: ComponentId, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::ComponentDeleted(ComponentDeletedPayload { component_id, change_set_id: ctx.change_set_id(), }), ) .await } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server