Skip to main content
Glama
values.rs16.2 kB
//! This module contains the ability to construct values reflecting the latest state of a //! [`Component`](crate::Component)'s properties. use std::collections::{ HashMap, VecDeque, }; use petgraph::prelude::*; use serde::{ Deserialize, Serialize, }; use serde_json::Value; use telemetry::prelude::*; use crate::{ AttributePrototype, AttributeValue, AttributeValueId, Component, ComponentId, DalContext, EdgeWeightKind, Prop, PropId, Secret, attribute::{ path::AttributePath, value::AttributeValueError, }, property_editor::{ PropertyEditorError, PropertyEditorPropId, PropertyEditorResult, PropertyEditorValueId, }, validation::{ ValidationOutput, ValidationOutputNode, }, }; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PropertyEditorValues { pub root_value_id: PropertyEditorValueId, pub values: HashMap<PropertyEditorValueId, PropertyEditorValue>, pub child_values: HashMap<PropertyEditorValueId, Vec<PropertyEditorValueId>>, } impl PropertyEditorValues { pub async fn assemble( ctx: &DalContext, component_id: ComponentId, ) -> PropertyEditorResult<Self> { let component = Component::get_by_id(ctx, component_id).await?; let controlling_ancestors_for_av_id = Component::list_av_controlling_func_ids_for_id(ctx, component_id).await?; let mut values = HashMap::new(); let mut child_values = HashMap::new(); // Get the root attribute value and load it into the work queue. let root_av_id = Component::root_attribute_value_id(ctx, component_id).await?; let root_value_id = PropertyEditorValueId::from(root_av_id); let root_prop_id = AttributeValue::prop_id(ctx, root_av_id).await?; let root_av = AttributeValue::get_by_id(ctx, root_av_id).await?; let validation = ValidationOutputNode::find_for_attribute_value_id(ctx, root_av_id) .await? .map(|node| node.validation); // Collect a map of all secret ids by key in the graph. In the future, we may want to cache // this or search while iterating. For now, the "list_ids_by_key_bench" test ensures that we // meet a baseline performance target. let secret_ids_by_key = { let start = tokio::time::Instant::now(); let secret_ids_by_key = Secret::list_ids_by_key(ctx).await?; debug!(%component_id, "listing secret ids by key took {:?}", start.elapsed()); secret_ids_by_key }; let secrets_av_id = Component::attribute_value_for_prop(ctx, component.id(), &["root", "secrets"]).await?; values.insert( root_value_id, PropertyEditorValue { id: root_value_id, prop_id: root_prop_id.into(), key: None, value: root_av.value_or_default_or_null(ctx, root_prop_id).await?, validation, is_from_external_source: false, can_be_set_by_socket: false, is_controlled_by_dynamic_func: false, is_controlled_by_ancestor: false, overridden: false, source: None, }, ); let mut work_queue = VecDeque::from([(root_av_id, root_value_id)]); while let Some((parent_av_id, parent_value_id)) = work_queue.pop_front() { // Now that we have the child props, prepare the property editor props and load the work queue. let mut child_value_ids = Vec::new(); for av_id in AttributeValue::get_child_av_ids_in_order(ctx, parent_av_id).await? { let key = AttributeValue::key_for_id(ctx, av_id).await?; // NOTE(nick): we already have the node weight, but I believe we still want to use "get_by_id" to // get the content from the store. Perhaps, there's a more efficient way that we can do this. let prop_id = AttributeValue::prop_id(ctx, av_id).await?; let value_id = PropertyEditorValueId::from(av_id); let sockets_for_av = AttributeValue::list_input_socket_sources_for_id(ctx, av_id).await?; let can_be_set_by_socket = !sockets_for_av.is_empty(); let controlling_func = *controlling_ancestors_for_av_id .get(&av_id) .ok_or(AttributeValueError::MissingForId(av_id))?; let controlling_prototype_id = AttributeValue::component_prototype_id(ctx, controlling_func.av_id).await?; let mut source = None; // Check if the component value is explicitly connected to another component. if let Some(controlling_prototype_id) = controlling_prototype_id { for apa_id in AttributePrototype::list_arguments(ctx, controlling_prototype_id).await? { for (edge, _, target) in ctx .workspace_snapshot()? .edges_directed(apa_id, Direction::Outgoing) .await? { if let EdgeWeightKind::ValueSubscription(AttributePath::JsonPointer( path, )) = edge.kind { let component = AttributeValue::component_id(ctx, target.into()).await?; source = Some(PropertyEditorValueSource::Subscription { component, path, }); } } } } // Note (victor): An attribute value is overridden if there is an attribute // prototype for this specific AV, which means it's set for the component, // not the schema variant. If the av is controlled, this check should be // made for its controlling AV. // This could be standalone func for AV, but we'd have to implement a // controlling_ancestors_for_av_id for av, instead of for the whole component. // Not a complicated task, but the PR that adds this has enough code as it is. let overridden = controlling_prototype_id.is_some(); let validation = ValidationOutputNode::find_for_attribute_value_id(ctx, av_id) .await? .map(|node| node.validation); // Get the value let mut value = AttributeValue::get_by_id(ctx, av_id) .await? .value_or_default_or_null(ctx, prop_id) .await?; // If this is a secret, the JSON value has the secret key, not the secret id. // The editor needs the secret id, so we look in our mapto find which Secret in // the current graph has that key. if parent_av_id == secrets_av_id && value != Value::Null { let secret_key = Secret::key_from_value_in_attribute_value(value)?; value = match secret_ids_by_key.get(&secret_key) { Some(secret_id) => serde_json::to_value(secret_id)?, None => { // If none of the secrets in the workspace have this key, we assume // that dependent values haven't updated yet and will be fixed // shortly. Thus we treat the property as missing for now and // return null. // // This is an expected issue, so we don't warn--but it could trigger // if something more serious is going on that is making the lookup // fail more persistently, so we may want to measure how often it // happens and figure out how to alert in that case. warn!( name: "Secret key does not match", av_id = %av_id, "Secret key in dependent value does not match any secret key; assuming that dependent values are not up to date and treating the property temporarily as missing", ); Value::Null } } } let value = PropertyEditorValue { id: value_id, prop_id: prop_id.into(), key, value, validation, can_be_set_by_socket, is_from_external_source: false, is_controlled_by_ancestor: controlling_func.av_id != av_id, is_controlled_by_dynamic_func: controlling_func.is_dynamic_func, overridden, source, }; // Load the work queue with the child attribute value. work_queue.push_back((av_id, value.id)); // Cache the child property editor values to eventually insert into the child property editor values map. child_value_ids.push(value.id); // Insert the child property editor value into the values map. values.insert(value.id, value); } child_values.insert(parent_value_id, child_value_ids); } Ok(Self { root_value_id, child_values, values, }) } /// Finds the [`AttributeValueId`](AttributeValue) for a given [`PropId`](Prop). /// /// This is useful for non-maps and non-array [`Props`](Prop). pub fn find_by_prop_id(&self, prop_id: PropId) -> Option<AttributeValueId> { self.values .iter() .find(|(_, property_editor_value)| property_editor_value.prop_id() == prop_id) .map(|(_, found_property_editor_value)| { found_property_editor_value.attribute_value_id() }) } /// Finds the [`AttributeValueId`](AttributeValue) for a given [`PropId`](Prop). /// /// This is useful for non-maps and non-array [`Props`](Prop). pub fn find_by_prop_id_or_err( &self, prop_id: PropId, ) -> PropertyEditorResult<AttributeValueId> { self.values .iter() .find(|(_, property_editor_value)| property_editor_value.prop_id() == prop_id) .map(|(_, found_property_editor_value)| { found_property_editor_value.attribute_value_id() }) .ok_or_else(|| PropertyEditorError::PropertyEditorValueNotFoundByPropId(prop_id)) } /// Finds the [`AttributeValueId`](AttributeValue) and the [`Value`] corresponding to it for a /// given [`PropId`](Prop). /// /// This is useful for non-maps and non-array [`Props`](Prop). pub fn find_with_value_by_prop_id(&self, prop_id: PropId) -> Option<(Value, AttributeValueId)> { self.values .iter() .find(|(_, property_editor_value)| property_editor_value.prop_id() == prop_id) .map(|(_, found_property_editor_value)| { ( found_property_editor_value.value.to_owned(), found_property_editor_value.attribute_value_id(), ) }) } /// Lists the [`AttributeValueIds`](AttributeValue) for a given [`PropId`](Prop). /// /// This is useful for map and array [`Props`](Prop). pub fn list_by_prop_id(&self, prop_id: PropId) -> Vec<AttributeValueId> { self.values .iter() .filter_map(|(_, property_editor_value)| { if property_editor_value.prop_id() == prop_id { Some(property_editor_value.attribute_value_id()) } else { None } }) .collect() } /// Lists the [`AttributeValueIds`](AttributeValue) and the [`Values`] corresponding to them for /// a given [`PropId`](Prop). /// /// This is useful for map and array [`Props`](Prop). pub fn list_with_values_by_prop_id(&self, prop_id: PropId) -> Vec<(Value, AttributeValueId)> { self.values .iter() .filter_map(|(_, property_editor_value)| { if property_editor_value.prop_id() == prop_id { Some(( property_editor_value.value(), property_editor_value.attribute_value_id(), )) } else { None } }) .collect() } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PropertyEditorValue { pub id: PropertyEditorValueId, pub prop_id: PropertyEditorPropId, pub key: Option<String>, pub value: Value, pub validation: Option<ValidationOutput>, pub can_be_set_by_socket: bool, // true if this prop value is currently driven by a socket, even if the socket isn't in use pub is_from_external_source: bool, // true if this prop has a value provided by a socket pub is_controlled_by_ancestor: bool, // if ancestor of prop is set by dynamic func, ID of ancestor that sets it pub is_controlled_by_dynamic_func: bool, // props driven by non-dynamic funcs have a statically set value pub overridden: bool, // true if this prop has a different controlling func id than the default for this asset #[serde(skip_serializing_if = "Option::is_none")] pub source: Option<PropertyEditorValueSource>, // The source of this value (set to None if it's a static value) } // The source for a value (unless it's a static value). // Try to keep this in sync with v2::component::attributes::Source #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub enum PropertyEditorValueSource { // We don't send this // // { value: <value> } - set value (null is a valid value to set it to) // Value(serde_json::Value), // { component: "ComponentNameOrId", path: "/domain/Foo/Bar/0/Baz" } - subscribe this value to a path from a component #[serde(untagged)] Subscription { component: ComponentId, path: String, }, } impl PropertyEditorValue { pub fn attribute_value_id(&self) -> AttributeValueId { self.id.into() } pub fn value(&self) -> Value { self.value.clone() } pub fn prop_id(&self) -> PropId { self.prop_id.into() } /// Returns the [`Prop`](crate::Prop) corresponding to the "prop_id" field. pub async fn prop(&self, ctx: &DalContext) -> PropertyEditorResult<Prop> { let prop = Prop::get_by_id(ctx, self.prop_id.into()).await?; Ok(prop) } } impl postgres_types::ToSql for PropertyEditorValues { fn to_sql( &self, ty: &postgres_types::Type, out: &mut postgres_types::private::BytesMut, ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> where Self: Sized, { let json = serde_json::to_value(self)?; postgres_types::ToSql::to_sql(&json, ty, out) } fn accepts(ty: &postgres_types::Type) -> bool where Self: Sized, { ty == &postgres_types::Type::JSONB } fn to_sql_checked( &self, ty: &postgres_types::Type, out: &mut postgres_types::private::BytesMut, ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> { let json = serde_json::to_value(self)?; postgres_types::ToSql::to_sql(&json, ty, out) } }

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