diff --git a/lib/dal/BUCK b/lib/dal/BUCK index 519a1fdac7..6a9b93c5a1 100644 --- a/lib/dal/BUCK +++ b/lib/dal/BUCK @@ -87,6 +87,7 @@ rust_test( "//lib/rebaser-core:rebaser-core", "//lib/rebaser-server:rebaser-server", "//lib/si-events-rs:si-events", + "//lib/si-frontend-types-rs:si-frontend-types", "//lib/si-pkg:si-pkg", "//lib/veritech-client:veritech-client", "//third-party/rust:chrono", diff --git a/lib/dal/src/action/prototype.rs b/lib/dal/src/action/prototype.rs index 29d3c6ad41..bbc77345ca 100644 --- a/lib/dal/src/action/prototype.rs +++ b/lib/dal/src/action/prototype.rs @@ -65,7 +65,7 @@ pub enum ActionPrototypeError { pub type ActionPrototypeResult = Result; #[remain::sorted] -#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Display)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Display, Hash)] pub enum ActionKind { /// Create the "outside world" version of the modeled object. Create, @@ -92,6 +92,17 @@ impl From for si_events::ActionKind { } } } +impl From for ActionKind { + fn from(value: si_events::ActionKind) -> Self { + match value { + si_events::ActionKind::Create => ActionKind::Create, + si_events::ActionKind::Destroy => ActionKind::Destroy, + si_events::ActionKind::Manual => ActionKind::Refresh, + si_events::ActionKind::Refresh => ActionKind::Refresh, + si_events::ActionKind::Update => ActionKind::Update, + } + } +} impl From for ActionKind { fn from(value: ActionFuncSpecKind) -> Self { diff --git a/lib/dal/src/attribute/prototype.rs b/lib/dal/src/attribute/prototype.rs index be10473850..2f63290e50 100644 --- a/lib/dal/src/attribute/prototype.rs +++ b/lib/dal/src/attribute/prototype.rs @@ -95,7 +95,7 @@ pub type AttributePrototypeResult = Result; #[remain::sorted] #[derive(Debug, Clone, Copy, EnumDiscriminants)] pub enum AttributePrototypeEventualParent { - Component(ComponentId), + Component(ComponentId, AttributeValueId), SchemaVariantFromInputSocket(SchemaVariantId, InputSocketId), SchemaVariantFromOutputSocket(SchemaVariantId, OutputSocketId), SchemaVariantFromProp(SchemaVariantId, PropId), @@ -105,6 +105,12 @@ pub enum AttributePrototypeEventualParent { // that the argument is a new one. pk!(AttributePrototypeId); +impl From for si_events::AttributePrototypeId { + fn from(value: AttributePrototypeId) -> Self { + si_events::AttributePrototypeId::from_raw_id(value.into()) + } +} + #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct AttributePrototype { pub id: AttributePrototypeId, @@ -544,11 +550,14 @@ impl AttributePrototype { let node_weight_id = node_weight.id(); let eventual_parent = match node_weight { - NodeWeight::AttributeValue(_) => AttributePrototypeEventualParent::Component( - AttributeValue::component_id(ctx, node_weight_id.into()) - .await - .map_err(Box::new)?, - ), + NodeWeight::AttributeValue(attribute_value_id) => { + AttributePrototypeEventualParent::Component( + AttributeValue::component_id(ctx, node_weight_id.into()) + .await + .map_err(Box::new)?, + attribute_value_id.id().into(), + ) + } NodeWeight::Prop(_) => AttributePrototypeEventualParent::SchemaVariantFromProp( SchemaVariant::find_for_prop_id(ctx, node_weight_id.into()) .await diff --git a/lib/dal/src/attribute/prototype/argument.rs b/lib/dal/src/attribute/prototype/argument.rs index 0c1e5ea9ec..75fb5e111d 100644 --- a/lib/dal/src/attribute/prototype/argument.rs +++ b/lib/dal/src/attribute/prototype/argument.rs @@ -45,6 +45,18 @@ pub mod value_source; // that the argument is a new one. pk!(AttributePrototypeArgumentId); +impl From for AttributePrototypeArgumentId { + fn from(value: si_events::AttributePrototypeArgumentId) -> Self { + Self(value.into_raw_id()) + } +} + +impl From for si_events::AttributePrototypeArgumentId { + fn from(value: AttributePrototypeArgumentId) -> Self { + Self::from_raw_id(value.0) + } +} + #[remain::sorted] #[derive(Error, Debug)] pub enum AttributePrototypeArgumentError { diff --git a/lib/dal/src/component.rs b/lib/dal/src/component.rs index 0b34e9561d..9220d08e5f 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -209,6 +209,12 @@ impl From for si_events::ComponentId { } } +impl From for ComponentId { + fn from(value: si_events::ComponentId) -> Self { + Self(value.into_raw_id()) + } +} + #[derive(Clone, Debug)] pub struct IncomingConnection { pub attribute_prototype_argument_id: AttributePrototypeArgumentId, diff --git a/lib/dal/src/func.rs b/lib/dal/src/func.rs index c6e50b21d3..048516a7cc 100644 --- a/lib/dal/src/func.rs +++ b/lib/dal/src/func.rs @@ -1,7 +1,11 @@ +use argument::{FuncArgument, FuncArgumentError}; +use authoring::FuncAuthoringError; use base64::{engine::general_purpose, Engine}; +use binding::{FuncBindings, FuncBindingsError}; use serde::{Deserialize, Serialize}; use si_events::CasValue; use si_events::{ulid::Ulid, ContentHash}; +use si_frontend_types::FuncSummary; use std::collections::HashMap; use std::string::FromUtf8Error; use std::sync::Arc; @@ -31,6 +35,7 @@ use self::backend::{FuncBackendKind, FuncBackendResponseType}; pub mod argument; pub mod authoring; pub mod backend; +pub mod binding; pub mod intrinsics; pub mod runner; pub mod summary; @@ -58,8 +63,14 @@ pub enum FuncError { ChronoParse(#[from] chrono::ParseError), #[error("edge weight error: {0}")] EdgeWeight(#[from] EdgeWeightError), + #[error("func argument error: {0}")] + FuncArgument(#[from] Box), #[error("func associations error: {0}")] FuncAssociations(#[from] Box), + #[error("func authoring client error: {0}")] + FuncAuthoringClient(#[from] Box), + #[error("func bindings error: {0}")] + FuncBindings(#[from] Box), #[error("func name already in use {0}")] FuncNameInUse(String), #[error("func to be deleted has associations: {0}")] @@ -630,14 +641,56 @@ impl Func { Ok(duplicated_func) } + + pub async fn into_frontend_type(&self, ctx: &DalContext) -> FuncResult { + let args = FuncArgument::list_for_func(ctx, self.id) + .await + .map_err(Box::new)?; + let mut arguments = vec![]; + for arg in args { + arguments.push(si_frontend_types::FuncArgument { + id: Some(arg.id.into()), + name: arg.name.clone(), + kind: arg.kind.into(), + element_kind: arg.element_kind.map(Into::into), + timestamp: arg.timestamp.into(), + }); + } + let bindings = FuncBindings::from_func_id(ctx, self.id) + .await + .map_err(Box::new)? + .into_frontend_type(); + Ok(FuncSummary { + func_id: self.id.into(), + kind: self.kind.into(), + name: self.name.clone(), + display_name: self.display_name.clone(), + is_locked: false, + arguments, + bindings, + }) + } } -#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +#[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FuncWsEventPayload { func_id: FuncId, change_set_id: ChangeSetId, } +#[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncWsEventFuncSummary { + change_set_id: ChangeSetId, + func_summary: si_frontend_types::FuncSummary, + types: String, +} +#[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncWsEventCodeSaved { + change_set_id: ChangeSetId, + func_code: si_frontend_types::FuncCode, +} impl WsEvent { pub async fn func_arguments_saved(ctx: &DalContext, func_id: FuncId) -> WsEventResult { @@ -672,4 +725,50 @@ impl WsEvent { ) .await } + + pub async fn func_updated( + ctx: &DalContext, + func_summary: si_frontend_types::FuncSummary, + types: String, + ) -> WsEventResult { + WsEvent::new( + ctx, + WsPayload::FuncUpdated(FuncWsEventFuncSummary { + change_set_id: ctx.change_set_id(), + func_summary, + types, + }), + ) + .await + } + + pub async fn func_created( + ctx: &DalContext, + func_summary: si_frontend_types::FuncSummary, + types: String, + ) -> WsEventResult { + WsEvent::new( + ctx, + WsPayload::FuncUpdated(FuncWsEventFuncSummary { + change_set_id: ctx.change_set_id(), + func_summary, + types, + }), + ) + .await + } + + pub async fn func_code_saved( + ctx: &DalContext, + func_code: si_frontend_types::FuncCode, + ) -> WsEventResult { + WsEvent::new( + ctx, + WsPayload::FuncCodeSaved(FuncWsEventCodeSaved { + change_set_id: ctx.change_set_id(), + func_code, + }), + ) + .await + } } diff --git a/lib/dal/src/func/argument.rs b/lib/dal/src/func/argument.rs index e4d372793a..1422364a85 100644 --- a/lib/dal/src/func/argument.rs +++ b/lib/dal/src/func/argument.rs @@ -135,8 +135,50 @@ impl From for PkgFuncArgumentKind { } } +impl From for FuncArgumentKind { + fn from(value: si_frontend_types::FuncArgumentKind) -> Self { + match value { + si_frontend_types::FuncArgumentKind::Any => FuncArgumentKind::Any, + si_frontend_types::FuncArgumentKind::Array => FuncArgumentKind::Array, + si_frontend_types::FuncArgumentKind::Boolean => FuncArgumentKind::Boolean, + si_frontend_types::FuncArgumentKind::Integer => FuncArgumentKind::Integer, + si_frontend_types::FuncArgumentKind::Json => FuncArgumentKind::Json, + si_frontend_types::FuncArgumentKind::Map => FuncArgumentKind::Map, + si_frontend_types::FuncArgumentKind::Object => FuncArgumentKind::Object, + si_frontend_types::FuncArgumentKind::String => FuncArgumentKind::String, + } + } +} + id!(FuncArgumentId); +impl From for FuncArgumentId { + fn from(value: si_events::FuncArgumentId) -> Self { + Self(value.into_raw_id()) + } +} + +impl From for si_events::FuncArgumentId { + fn from(value: FuncArgumentId) -> Self { + Self::from_raw_id(value.0) + } +} + +impl From for si_frontend_types::FuncArgumentKind { + fn from(value: FuncArgumentKind) -> Self { + match value { + FuncArgumentKind::Any => si_frontend_types::FuncArgumentKind::Any, + FuncArgumentKind::Array => si_frontend_types::FuncArgumentKind::Array, + FuncArgumentKind::Boolean => si_frontend_types::FuncArgumentKind::Boolean, + FuncArgumentKind::Integer => si_frontend_types::FuncArgumentKind::Integer, + FuncArgumentKind::Json => si_frontend_types::FuncArgumentKind::Json, + FuncArgumentKind::Map => si_frontend_types::FuncArgumentKind::Map, + FuncArgumentKind::Object => si_frontend_types::FuncArgumentKind::Object, + FuncArgumentKind::String => si_frontend_types::FuncArgumentKind::String, + } + } +} + #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct FuncArgument { pub id: FuncArgumentId, diff --git a/lib/dal/src/func/associations.rs b/lib/dal/src/func/associations.rs index b2106cce49..f7bc80ea9e 100644 --- a/lib/dal/src/func/associations.rs +++ b/lib/dal/src/func/associations.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use strum::EnumDiscriminants; use telemetry::prelude::*; @@ -87,8 +88,17 @@ impl FuncAssociations { ) -> FuncAssociationsResult<(Option, String)> { let (associations, input_type) = match func.kind { FuncKind::Action => { - let schema_variant_ids = SchemaVariant::list_for_action_func(ctx, func.id).await?; - let action_prototype_ids = ActionPrototype::list_for_func_id(ctx, func.id).await?; + let schemas_and_prototypes = + SchemaVariant::list_for_action_func(ctx, func.id).await?; + let schema_variant_ids = schemas_and_prototypes + .clone() + .into_iter() + .map(|(sv, _)| sv) + .collect_vec(); + let action_prototype_ids = schemas_and_prototypes + .into_iter() + .map(|(_, ap)| ap) + .collect_vec(); // TODO(nick): right now, we just grab the first one and it decides the action kind for all of them. // This should be configurable on a "per prototype" basis in the future. @@ -154,7 +164,7 @@ impl FuncAssociations { AttributePrototype::eventual_parent(ctx, attribute_prototype_id).await?; match eventual_parent { - AttributePrototypeEventualParent::Component(component_id) => { + AttributePrototypeEventualParent::Component(component_id, _) => { component_ids.push(component_id) } AttributePrototypeEventualParent::SchemaVariantFromInputSocket( @@ -200,7 +210,7 @@ impl FuncAssociations { let eventual_parent = AttributePrototype::eventual_parent(ctx, attribute_prototype_id).await?; match eventual_parent { - AttributePrototypeEventualParent::Component(component_id) => { + AttributePrototypeEventualParent::Component(component_id, _) => { component_ids.push(component_id) } AttributePrototypeEventualParent::SchemaVariantFromInputSocket( diff --git a/lib/dal/src/func/associations/bags.rs b/lib/dal/src/func/associations/bags.rs index c05109ec27..182d0432fd 100644 --- a/lib/dal/src/func/associations/bags.rs +++ b/lib/dal/src/func/associations/bags.rs @@ -85,7 +85,7 @@ impl AttributePrototypeBag { ) -> FuncAssociationsResult { let eventual_parent = AttributePrototype::eventual_parent(ctx, id).await?; let (component_id, schema_variant_id, prop_id, output_socket_id) = match eventual_parent { - AttributePrototypeEventualParent::Component(component_id) => { + AttributePrototypeEventualParent::Component(component_id, _) => { (Some(component_id), None, None, None) } AttributePrototypeEventualParent::SchemaVariantFromInputSocket( diff --git a/lib/dal/src/func/authoring.rs b/lib/dal/src/func/authoring.rs index 8550c7768c..be7d35a7ff 100644 --- a/lib/dal/src/func/authoring.rs +++ b/lib/dal/src/func/authoring.rs @@ -57,6 +57,7 @@ use crate::func::associations::{FuncAssociations, FuncAssociationsError}; use crate::func::view::FuncViewError; use crate::func::FuncKind; use crate::prop::PropError; +use crate::schema::variant::leaves::{LeafInputLocation, LeafKind}; use crate::socket::output::OutputSocketError; use crate::{ AttributePrototype, AttributePrototypeId, ComponentError, ComponentId, DalContext, Func, @@ -65,6 +66,14 @@ use crate::{ WsEventError, }; +use super::binding::action::ActionBinding; +use super::binding::attribute::AttributeBinding; +use super::binding::authentication::AuthBinding; +use super::binding::leaf::LeafBinding; +use super::binding::{ + AttributeArgumentBinding, AttributeFuncDestination, EventualParent, FuncBindings, + FuncBindingsError, +}; use super::runner::{FuncRunner, FuncRunnerError}; use super::{AttributePrototypeArgumentBag, AttributePrototypeBag}; @@ -95,6 +104,8 @@ pub enum FuncAuthoringError { FuncArgument(#[from] FuncArgumentError), #[error("func associations error: {0}")] FuncAssociations(#[from] FuncAssociationsError), + #[error("func bindings error: {0}")] + FuncBindings(#[from] FuncBindingsError), #[error("func ({0}) with kind ({1}) cannot have associations: {2:?}")] FuncCannotHaveAssociations(FuncId, FuncKind, FuncAssociations), #[error("func named \"{0}\" already exists in this change set")] @@ -165,6 +176,108 @@ impl FuncAuthoringClient { code: func.code_plaintext()?, }) } + /// Creates a new Attribute Func and returns it + #[instrument( + name = "func.authoring.create_new_attribute_func", + level = "info", + skip(ctx) + )] + + pub async fn create_new_attribute_func( + ctx: &DalContext, + name: Option, + eventual_parent: Option, + output_location: AttributeFuncDestination, + argument_bindings: Vec, + ) -> FuncAuthoringResult { + let func = create::create(ctx, FuncKind::Attribute, name, None).await?; + AttributeBinding::upsert_attribute_binding( + ctx, + func.id, + eventual_parent, + output_location, + argument_bindings, + ) + .await?; + Ok(func) + } + + /// Creates a new Action Func and returns it + #[instrument( + name = "func.authoring.create_new_action_func", + level = "info", + skip(ctx) + )] + + pub async fn create_new_action_func( + ctx: &DalContext, + name: Option, + action_kind: ActionKind, + schema_variant_id: SchemaVariantId, + ) -> FuncAuthoringResult { + let func = create::create(ctx, FuncKind::Action, name, None).await?; + ActionBinding::create_action_binding(ctx, func.id, action_kind, schema_variant_id).await?; + Ok(func) + } + + /// Creates a new Code Gen or Qualification Func and returns it + #[instrument( + name = "func.authoring.create_new_leaf_func", + level = "info", + skip(ctx) + )] + + pub async fn create_new_leaf_func( + ctx: &DalContext, + name: Option, + leaf_kind: LeafKind, + eventual_parent: EventualParent, + inputs: &[LeafInputLocation], + ) -> FuncAuthoringResult { + let func = match leaf_kind { + LeafKind::CodeGeneration => { + let func = create::create(ctx, FuncKind::CodeGeneration, name, None).await?; + LeafBinding::create_leaf_func_binding( + ctx, + func.id, + eventual_parent, + leaf_kind, + inputs, + ) + .await?; + func + } + LeafKind::Qualification => { + let func = create::create(ctx, FuncKind::Qualification, name, None).await?; + LeafBinding::create_leaf_func_binding( + ctx, + func.id, + eventual_parent, + leaf_kind, + inputs, + ) + .await?; + func + } + }; + Ok(func) + } + + /// Create a new Auth func and return it + #[instrument( + name = "func.authoring.create_new_auth_func", + level = "info", + skip(ctx) + )] + pub async fn create_new_auth_func( + ctx: &DalContext, + name: Option, + schema_variant_id: SchemaVariantId, + ) -> FuncAuthoringResult { + let func = create::create(ctx, FuncKind::Authentication, name, None).await?; + AuthBinding::create_auth_binding(ctx, func.id, schema_variant_id).await?; + Ok(func) + } /// Performs a "test" [`Func`] execution and returns the [`FuncRunId`](si_events::FuncRun). #[instrument(name = "func.authoring.test_execute_func", level = "info", skip(ctx))] @@ -291,6 +404,7 @@ impl FuncAuthoringClient { /// Creates an [`AttributePrototype`]. Used when attaching an existing attribute /// function to a schema variant and/or component + /// todo: remove once front end consumes new routes #[instrument( name = "func.authoring.create_attribute_prototype", level = "info", @@ -331,6 +445,7 @@ impl FuncAuthoringClient { } /// Updates an [`AttributePrototype`]. + /// todo: remove once front end consumes new routes #[instrument( name = "func.authoring.update_attribute_prototype", level = "info", @@ -372,6 +487,7 @@ impl FuncAuthoringClient { } /// Removes an [`AttributePrototype`]. + /// todo: remove once front end consumes new routes #[instrument( name = "func.authoring.remove_attribute_prototype", level = "info", @@ -403,6 +519,7 @@ impl FuncAuthoringClient { } /// Saves a [`Func`]. + /// todo: remove once front end consumes new routes #[instrument(name = "func.authoring.save_func", level = "info", skip(ctx))] pub async fn save_func( ctx: &DalContext, @@ -441,6 +558,7 @@ impl FuncAuthoringClient { /// For a given [`FuncId`], regardless of what kind of [`Func`] it is, look for all associated bindings and remove /// them from every currently attached [`SchemaVariant`] + /// todo: remove once front end consumes new routes pub async fn detach_func_from_everywhere( ctx: &DalContext, func_id: FuncId, @@ -466,6 +584,43 @@ impl FuncAuthoringClient { Ok(()) } + #[instrument(level = "info", name = "func.authoring.save_code", skip(ctx))] + /// Save only the code for the given [`FuncId`] + pub async fn save_code( + ctx: &DalContext, + func_id: FuncId, + code: String, + ) -> FuncAuthoringResult<()> { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + + Func::modify_by_id(ctx, func.id, |func| { + func.code_base64 = Some(general_purpose::STANDARD_NO_PAD.encode(code)); + + Ok(()) + }) + .await?; + Ok(()) + } + + #[instrument(level = "info", name = "func.authoring.update_func", skip(ctx))] + /// Save metadata about the [`FuncId`] + pub async fn update_func( + ctx: &DalContext, + func_id: FuncId, + display_name: Option, + description: Option, + ) -> FuncAuthoringResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + + let updated_func = Func::modify_by_id(ctx, func.id, |func| { + display_name.clone_into(&mut func.display_name); + description.clone_into(&mut func.description); + Ok(()) + }) + .await?; + Ok(updated_func) + } + /// Compiles types corresponding to "lang-js". pub fn compile_langjs_types() -> &'static str { ts_types::compile_langjs_types() @@ -478,6 +633,14 @@ impl FuncAuthoringClient { ) -> &'static str { ts_types::compile_return_types(response_type, kind) } + + /// Comiples return types based on the [`FuncBindings`] for the Func + pub async fn compile_types_from_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncAuthoringResult { + Ok(FuncBindings::compile_types(ctx, func_id).await?) + } } /// The result of creating a [`Func`] via [`FuncAuthoringClient::create_func`]. diff --git a/lib/dal/src/func/binding.rs b/lib/dal/src/func/binding.rs new file mode 100644 index 0000000000..6fe884434d --- /dev/null +++ b/lib/dal/src/func/binding.rs @@ -0,0 +1,561 @@ +use action::ActionBinding; +use attribute::AttributeBinding; +use authentication::AuthBinding; +use itertools::Itertools; +use leaf::LeafBinding; +use serde::{Deserialize, Serialize}; +use strum::EnumDiscriminants; +use telemetry::prelude::*; +use thiserror::Error; + +use crate::action::prototype::{ActionKind, ActionPrototypeError}; +use crate::attribute::prototype::argument::value_source::ValueSource; +use crate::attribute::prototype::argument::{ + AttributePrototypeArgumentError, AttributePrototypeArgumentId, +}; +use crate::attribute::value::AttributeValueError; +use crate::socket::output::OutputSocketError; +use crate::{ + ChangeSetId, ComponentId, OutputSocketId, SchemaVariant, WorkspaceSnapshotError, WsEvent, + WsEventError, WsEventResult, WsPayload, +}; +pub use attribute_argument::AttributeArgumentBinding; +pub use attribute_argument::AttributeFuncArgumentSource; + +use crate::attribute::prototype::AttributePrototypeError; +use crate::func::argument::FuncArgumentError; +use crate::func::FuncKind; +use crate::prop::PropError; +use crate::schema::variant::leaves::LeafInputLocation; +use crate::{ + socket::input::InputSocketError, ActionPrototypeId, AttributePrototypeId, ComponentError, + DalContext, Func, FuncError, FuncId, PropId, SchemaVariantError, SchemaVariantId, +}; + +use super::argument::FuncArgumentId; + +pub mod action; +pub mod attribute; +pub mod attribute_argument; +pub mod authentication; +pub mod leaf; + +#[remain::sorted] +#[derive(Error, Debug)] +pub enum FuncBindingsError { + #[error("action with kind ({0}) already exists for schema variant ({1}), cannot have two non-manual actions for the same kind in the same schema variant")] + ActionKindAlreadyExists(ActionKind, SchemaVariantId), + #[error("action prototype error: {0}")] + ActionPrototype(#[from] ActionPrototypeError), + #[error("action prototype missing")] + ActionPrototypeMissing, + #[error("attribute prototype error: {0}")] + AttributePrototype(#[from] AttributePrototypeError), + #[error("attribute prototype argument error: {0}")] + AttributePrototypeArgument(#[from] AttributePrototypeArgumentError), + #[error("attribute prototype missing")] + AttributePrototypeMissing, + #[error("attribute value error: {0}")] + AttributeValue(#[from] AttributeValueError), + #[error("cannot compile types for func: {0}")] + CannotCompileTypes(FuncId), + #[error("component error: {0}")] + ComponentError(#[from] ComponentError), + #[error("failed to remove attribute value for leaf")] + FailedToRemoveLeafAttributeValue, + #[error("func error: {0}")] + Func(#[from] FuncError), + #[error("func argument error: {0}")] + FuncArgument(#[from] FuncArgumentError), + #[error("input socket error: {0}")] + InputSocket(#[from] InputSocketError), + #[error("malformed input for binding: {0}")] + MalformedInput(String), + #[error("missing value source for attribute prototype argument id {0}")] + MissingValueSource(AttributePrototypeArgumentId), + #[error("no input location given for attribute prototype id ({0}) and func argument id ({1})")] + NoInputLocationGiven(AttributePrototypeId, FuncArgumentId), + #[error("no output location given for func: {0}")] + NoOutputLocationGiven(FuncId), + #[error("output socket error: {0}")] + OutputSocket(#[from] OutputSocketError), + #[error("prop error: {0}")] + Prop(#[from] PropError), + #[error("schema variant error: {0}")] + SchemaVariant(#[from] SchemaVariantError), + #[error("serde error: {0}")] + Serde(#[from] serde_json::Error), + #[error("unexpected func binding variant: {0:?} (expected: {1:?})")] + UnexpectedFuncBindingVariant(FuncBindingDiscriminants, FuncBindingDiscriminants), + #[error("unexpected func kind ({0}) creating attribute func")] + UnexpectedFuncKind(FuncKind), + #[error("unexpected value source ({0:?}) for attribute prototype argument: {1}")] + UnexpectedValueSource(ValueSource, AttributePrototypeArgumentId), + #[error("workspace snapshot error: {0}")] + WorkspaceSnapshot(#[from] WorkspaceSnapshotError), + #[error("ws event error: {0}")] + WsEvent(#[from] WsEventError), +} +type FuncBindingsResult = Result; + +/// Represents the location where the function ultimately writes to +/// We currently only allow Attribute Funcs to be attached to Props +/// (or the attribute value in the case of a component) and Output Sockets +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AttributeFuncDestination { + Prop(PropId), + OutputSocket(OutputSocketId), +} + +impl AttributeFuncDestination { + pub(crate) async fn find_schema_variant( + &self, + ctx: &DalContext, + ) -> FuncBindingsResult { + let schema_variant_id = match self { + AttributeFuncDestination::Prop(prop_id) => { + SchemaVariant::find_for_prop_id(ctx, *prop_id).await? + } + AttributeFuncDestination::OutputSocket(output_socket_id) => { + SchemaVariant::find_for_output_socket_id(ctx, *output_socket_id).await? + } + }; + Ok(schema_variant_id) + } +} + +/// Represents at what level a given Prototype is attached to +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventualParent { + SchemaVariant(SchemaVariantId), + Component(ComponentId), +} + +impl From for Option { + fn from(value: EventualParent) -> Self { + match value { + EventualParent::SchemaVariant(_) => None, + EventualParent::Component(component_id) => Some(component_id.into()), + } + } +} + +impl From for Option { + fn from(value: EventualParent) -> Self { + match value { + EventualParent::SchemaVariant(schema_variant_id) => Some(schema_variant_id.into()), + EventualParent::Component(_) => None, + } + } +} + +impl From for Option { + fn from(value: AttributeFuncDestination) -> Self { + match value { + AttributeFuncDestination::Prop(prop_id) => Some(prop_id.into()), + _ => None, + } + } +} + +impl From for Option { + fn from(value: AttributeFuncDestination) -> Self { + match value { + AttributeFuncDestination::OutputSocket(output_socket_id) => { + Some(output_socket_id.into()) + } + _ => None, + } + } +} + +impl From for si_frontend_types::FuncBinding { + fn from(value: FuncBinding) -> Self { + match value { + FuncBinding::Action { + schema_variant_id, + action_prototype_id, + func_id, + kind, + } => si_frontend_types::FuncBinding::Action { + schema_variant_id: Some(schema_variant_id.into()), + action_prototype_id: Some(action_prototype_id.into()), + func_id: Some(func_id.into()), + kind: Some(kind.into()), + }, + FuncBinding::Attribute { + func_id, + attribute_prototype_id, + eventual_parent, + output_location, + argument_bindings, + } => si_frontend_types::FuncBinding::Attribute { + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + component_id: eventual_parent.into(), + schema_variant_id: eventual_parent.into(), + prop_id: output_location.into(), + output_socket_id: output_location.into(), + argument_bindings: argument_bindings + .into_iter() + .map(|arg| si_frontend_types::AttributeArgumentBinding { + func_argument_id: arg.func_argument_id.into(), + attribute_prototype_argument_id: arg + .attribute_prototype_argument_id + .map(Into::into), + prop_id: arg.attribute_func_input_location.clone().into(), + input_socket_id: arg.attribute_func_input_location.clone().into(), + }) + .collect_vec(), + }, + FuncBinding::Authentication { + schema_variant_id, + func_id, + } => si_frontend_types::FuncBinding::Authentication { + schema_variant_id: schema_variant_id.into(), + func_id: func_id.into(), + }, + FuncBinding::CodeGeneration { + func_id, + attribute_prototype_id, + eventual_parent, + inputs, + } => si_frontend_types::FuncBinding::CodeGeneration { + schema_variant_id: eventual_parent.into(), + component_id: eventual_parent.into(), + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + inputs: inputs.into_iter().map(|input| input.into()).collect_vec(), + }, + FuncBinding::Qualification { + func_id, + attribute_prototype_id, + eventual_parent, + inputs, + } => si_frontend_types::FuncBinding::Qualification { + schema_variant_id: eventual_parent.into(), + component_id: eventual_parent.into(), + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + inputs: inputs.into_iter().map(|input| input.into()).collect_vec(), + }, + } + } +} + +/// A [`FuncBinding`] represents the intersection of a function and the [`SchemaVariant`] (or [`Component`]) +/// specific information required to know when to run a function, what it's inputs are, and where the result of +/// the function is recorded +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, EnumDiscriminants, Hash)] +pub enum FuncBinding { + /// An Action function can only (currently) be attached to a [`Schema Variant`] + /// The [`ActionPrototypeId`] represents the unique relationship to a particular [`SchemaVariant`] + /// Interestingly, the [`ActionKind`] is a property on the [`ActionPrototype`], meaning changing the + /// [`ActionKind`] does not require any changes to the [`Func`] itself. + Action { + // unique ids + schema_variant_id: SchemaVariantId, + action_prototype_id: ActionPrototypeId, + func_id: FuncId, + //thing that can be updated + kind: ActionKind, + }, + /// An Attribute function is a function that sets values within a [`SchemaVariant`] or [`Component`]'s Prop Tree + /// Each Attribute Function has user defined arguments, configured to map to specific Props or InputSockets. + /// This intersection of [`FuncArgument`] and the [`Prop`] or [`InputSocket`] mapping is an [`AttributeArgumentBinding`] + Attribute { + // unique ids + func_id: FuncId, + attribute_prototype_id: AttributePrototypeId, + // things needed for create + eventual_parent: EventualParent, + + // things that can be updated + output_location: AttributeFuncDestination, + argument_bindings: Vec, + }, + /// Auth Funcs only exist on Secret defining schema variants, and have no special configuration data aside from the + /// [`SchemaVariantId`] and as such are only created or deleted (detached), they are not updated. + Authentication { + // unique ids + schema_variant_id: SchemaVariantId, + func_id: FuncId, + }, + /// CodeGen funcs are ultimately just an Attribute Function, but the user can not control where they output to. + /// They write to an Attribute Value beneath the Code Gen Root Prop Node + CodeGeneration { + // unique ids + func_id: FuncId, + attribute_prototype_id: AttributePrototypeId, + // things needed for create + eventual_parent: EventualParent, + // thing that can be updated + inputs: Vec, + }, + /// Qualification funcs are ultimately just an Attribute Function, but the user can not control where they output to. + /// They write to an Attribute Value beneath the Qualification Root Prop Node + Qualification { + // unique ids + func_id: FuncId, + attribute_prototype_id: AttributePrototypeId, + // things needed for create + eventual_parent: EventualParent, + // thing that can be updated + inputs: Vec, + }, +} +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct FuncBindings { + pub bindings: Vec, +} + +impl FuncBindings { + /// converts this enum to the front end type + pub fn into_frontend_type(&self) -> si_frontend_types::FuncBindings { + let mut front_end_bindings = Vec::with_capacity(self.bindings.len()); + for binding in &self.bindings { + let front_end_binding = match binding.clone() { + FuncBinding::Action { + schema_variant_id, + action_prototype_id, + func_id, + kind, + } => si_frontend_types::FuncBinding::Action { + schema_variant_id: Some(schema_variant_id.into()), + action_prototype_id: Some(action_prototype_id.into()), + func_id: Some(func_id.into()), + kind: Some(kind.into()), + }, + FuncBinding::Attribute { + func_id, + attribute_prototype_id, + eventual_parent, + output_location, + argument_bindings, + } => si_frontend_types::FuncBinding::Attribute { + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + component_id: eventual_parent.into(), + schema_variant_id: eventual_parent.into(), + prop_id: output_location.into(), + output_socket_id: output_location.into(), + argument_bindings: argument_bindings + .into_iter() + .map(|f| f.into_frontend_type()) + .collect_vec(), + }, + FuncBinding::Authentication { + schema_variant_id, + func_id, + } => si_frontend_types::FuncBinding::Authentication { + schema_variant_id: schema_variant_id.into(), + func_id: func_id.into(), + }, + FuncBinding::CodeGeneration { + func_id, + attribute_prototype_id, + eventual_parent, + inputs, + } => si_frontend_types::FuncBinding::CodeGeneration { + schema_variant_id: eventual_parent.into(), + component_id: eventual_parent.into(), + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + inputs: inputs.into_iter().map(|input| input.into()).collect_vec(), + }, + FuncBinding::Qualification { + func_id, + attribute_prototype_id, + eventual_parent, + inputs, + } => si_frontend_types::FuncBinding::Qualification { + schema_variant_id: eventual_parent.into(), + component_id: eventual_parent.into(), + func_id: Some(func_id.into()), + attribute_prototype_id: Some(attribute_prototype_id.into()), + inputs: inputs.into_iter().map(|input| input.into()).collect_vec(), + }, + }; + front_end_bindings.push(front_end_binding); + } + si_frontend_types::FuncBindings { + bindings: front_end_bindings, + } + } + + /// For a given [`FuncId`], gather all of the bindings for every place where it is being used + #[instrument( + level = "debug", + skip(ctx), + name = "func.binding.delete_all_bindings_for_func_id" + )] + pub async fn from_func_id( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + let bindings: Vec = match func.kind { + FuncKind::Action => ActionBinding::assemble_action_bindings(ctx, func_id).await?, + FuncKind::Attribute => { + AttributeBinding::assemble_attribute_bindings(ctx, func_id).await? + } + FuncKind::Authentication => AuthBinding::assemble_auth_bindings(ctx, func_id).await?, + FuncKind::CodeGeneration => { + LeafBinding::assemble_code_gen_bindings(ctx, func_id).await? + } + FuncKind::Qualification => { + LeafBinding::assemble_qualification_bindings(ctx, func_id).await? + } + + FuncKind::Intrinsic | FuncKind::SchemaVariantDefinition | FuncKind::Unknown => { + //debug!(?func.kind, "no associations or input type needed for func kind"); + vec![] + } + }; + Ok(FuncBindings { bindings }) + } + + /// Removes all existing bindings for a given [`FuncId`] + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.delete_all_bindings_for_func_id" + )] + pub async fn delete_all_bindings_for_func_id( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult<()> { + let func_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + for binding in func_bindings.bindings { + match binding { + FuncBinding::Action { + action_prototype_id, + .. + } => ActionBinding::delete_action_binding(ctx, action_prototype_id).await?, + FuncBinding::Attribute { + attribute_prototype_id, + .. + } => AttributeBinding::reset_attribute_binding(ctx, attribute_prototype_id).await?, + FuncBinding::Authentication { + schema_variant_id, + func_id, + } => AuthBinding::delete_auth_binding(ctx, func_id, schema_variant_id).await?, + FuncBinding::CodeGeneration { + attribute_prototype_id, + .. + } => LeafBinding::delete_leaf_func_binding(ctx, attribute_prototype_id).await?, + FuncBinding::Qualification { + attribute_prototype_id, + .. + } => LeafBinding::delete_leaf_func_binding(ctx, attribute_prototype_id).await?, + }; + } + Ok(()) + } + + /// Compile all the types for all of the bindings to return to the front end for type checking + pub async fn compile_types(ctx: &DalContext, func_id: FuncId) -> FuncBindingsResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + let types: String = match func.kind { + FuncKind::Action => ActionBinding::compile_action_types(ctx, func_id).await?, + FuncKind::CodeGeneration | FuncKind::Qualification => { + LeafBinding::compile_leaf_func_types(ctx, func_id).await? + } + FuncKind::Attribute => AttributeBinding::compile_attribute_types(ctx, func_id).await?, + FuncKind::Authentication + | FuncKind::Intrinsic + | FuncKind::SchemaVariantDefinition + | FuncKind::Unknown => String::new(), + }; + Ok(types) + } + + /// Get the ActionBinding if it exists, otherwise return an error. Useful for tests + pub fn get_action_internals(&self) -> FuncBindingsResult> { + let mut actions = vec![]; + for binding in self.bindings.clone() { + match binding { + FuncBinding::Action { + schema_variant_id, + kind, + .. + } => actions.push((kind, schema_variant_id)), + other_binding => { + return Err(FuncBindingsError::UnexpectedFuncBindingVariant( + other_binding.into(), + FuncBindingDiscriminants::Action, + )) + } + } + } + Ok(actions) + } + + // this func is just for integration tests. + #[allow(clippy::type_complexity)] + /// Get the Attribute Binding if it exists, otherwise return an error. Useful for tests + pub fn get_attribute_internals( + &self, + ) -> FuncBindingsResult< + Vec<( + AttributePrototypeId, + EventualParent, + AttributeFuncDestination, + Vec, + )>, + > { + let mut attributes = vec![]; + for binding in self.bindings.clone() { + match binding { + FuncBinding::Attribute { + func_id: _, + attribute_prototype_id, + eventual_parent, + output_location, + argument_bindings, + } => { + attributes.push(( + attribute_prototype_id, + eventual_parent, + output_location, + argument_bindings, + )); + } + other_binding => { + return Err(FuncBindingsError::UnexpectedFuncBindingVariant( + other_binding.into(), + FuncBindingDiscriminants::Authentication, + )) + } + } + } + Ok(attributes) + } +} +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct FuncBindingsWsEventPayload { + change_set_id: ChangeSetId, + #[serde(flatten)] + bindings: si_frontend_types::FuncBindings, + types: String, +} + +impl WsEvent { + pub async fn func_bindings_updated( + ctx: &DalContext, + bindings: si_frontend_types::FuncBindings, + types: String, + ) -> WsEventResult { + WsEvent::new( + ctx, + WsPayload::FuncBindingsUpdated(FuncBindingsWsEventPayload { + change_set_id: ctx.change_set_id(), + bindings, + types, + }), + ) + .await + } +} diff --git a/lib/dal/src/func/binding/action.rs b/lib/dal/src/func/binding/action.rs new file mode 100644 index 0000000000..3f762e332e --- /dev/null +++ b/lib/dal/src/func/binding/action.rs @@ -0,0 +1,148 @@ +use telemetry::prelude::*; + +use crate::{ + action::prototype::{ActionKind, ActionPrototype}, + func::binding::FuncBindingsError, + prop::PropPath, + ActionPrototypeId, DalContext, Func, FuncId, Prop, SchemaVariant, SchemaVariantError, + SchemaVariantId, +}; + +use super::{FuncBinding, FuncBindings, FuncBindingsResult}; +pub struct ActionBinding; + +impl ActionBinding { + pub(crate) async fn assemble_action_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + let schema_variant_ids = SchemaVariant::list_for_action_func(ctx, func_id).await?; + let mut bindings = vec![]; + for (schema_variant_id, action_prototype_id) in schema_variant_ids { + let action_prototype = ActionPrototype::get_by_id(ctx, action_prototype_id).await?; + bindings.push(FuncBinding::Action { + kind: action_prototype.kind, + schema_variant_id, + action_prototype_id, + func_id, + }); + } + Ok(bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.action.update_action_binding" + )] + /// Updates the [`ActionKind`] for a given [`ActionPrototypeId`] by removing the existing [`ActionPrototype`] + /// and creating a new one in its place + pub async fn update_action_binding( + ctx: &DalContext, + action_prototype_id: ActionPrototypeId, + kind: ActionKind, + ) -> FuncBindingsResult { + let schema_variant_id = + ActionPrototype::schema_variant_id(ctx, action_prototype_id).await?; + let func_id = ActionPrototype::func_id(ctx, action_prototype_id).await?; + let func = Func::get_by_id_or_error(ctx, func_id).await?; + // delete and recreate the prototype + //brit todo: there might be existing actions enqueued, we should find them and reassociate the prototype + ActionPrototype::remove(ctx, action_prototype_id).await?; + ActionPrototype::new( + ctx, + kind, + func.name.to_owned(), + func.description.to_owned(), + schema_variant_id, + func_id, + ) + .await?; + let new_binding = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_binding) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.action.create_action_binding" + )] + /// Creates an [`ActionPrototype`] with the specified [`ActionKind`] for a given [`SchemaVariantId`] + /// Checks to ensure there isn't already an Action with that Kind in the case of Create/Delete/Refresh + pub async fn create_action_binding( + ctx: &DalContext, + func_id: FuncId, + action_kind: ActionKind, + schema_variant_id: SchemaVariantId, + ) -> FuncBindingsResult { + let existing_action_prototypes = + ActionPrototype::for_variant(ctx, schema_variant_id).await?; + if action_kind != ActionKind::Manual + && existing_action_prototypes + .iter() + .any(|p| p.kind == action_kind) + { + return Err(FuncBindingsError::ActionKindAlreadyExists( + action_kind, + schema_variant_id, + )); + } + let func = Func::get_by_id_or_error(ctx, func_id).await?; + ActionPrototype::new( + ctx, + action_kind, + func.name.to_owned(), + func.description.to_owned(), + schema_variant_id, + func.id, + ) + .await?; + + let new_binding = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_binding) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.action.delete_action_binding" + )] + /// Deletes an [`ActionPrototype`] by the [`ActionPrototypeId`] + pub async fn delete_action_binding( + ctx: &DalContext, + action_prototype_id: ActionPrototypeId, + ) -> FuncBindingsResult { + let func_id = ActionPrototype::func_id(ctx, action_prototype_id).await?; + + ActionPrototype::remove(ctx, action_prototype_id).await?; + + FuncBindings::from_func_id(ctx, func_id).await + } + + pub(crate) async fn compile_action_types( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult { + let schema_variant_ids = SchemaVariant::list_for_action_func(ctx, func_id).await?; + let mut ts_types = vec![]; + for (variant_id, _) in schema_variant_ids { + let path = "root"; + let prop = match Prop::find_prop_by_path(ctx, variant_id, &PropPath::new([path])).await + { + Ok(prop_id) => prop_id, + Err(_) => Err(SchemaVariantError::PropNotFoundAtPath( + variant_id, + path.to_string(), + ))?, + }; + ts_types.push(prop.ts_type(ctx).await?) + } + Ok(format!( + "type Input {{ + kind: 'standard'; + properties: {}; + }}", + ts_types.join(" | "), + )) + } +} diff --git a/lib/dal/src/func/binding/attribute.rs b/lib/dal/src/func/binding/attribute.rs new file mode 100644 index 0000000000..13a35ee364 --- /dev/null +++ b/lib/dal/src/func/binding/attribute.rs @@ -0,0 +1,500 @@ +use std::collections::HashMap; +use telemetry::prelude::*; + +use crate::{ + attribute::prototype::{ + argument::AttributePrototypeArgument, AttributePrototypeEventualParent, + }, + func::{ + argument::{FuncArgument, FuncArgumentError}, + intrinsics::IntrinsicFunc, + FuncKind, + }, + workspace_snapshot::graph::WorkspaceSnapshotGraphError, + AttributePrototype, AttributePrototypeId, AttributeValue, Component, DalContext, + EdgeWeightKind, Func, FuncId, OutputSocket, Prop, WorkspaceSnapshotError, +}; + +use super::{ + AttributeArgumentBinding, AttributeFuncArgumentSource, AttributeFuncDestination, + EventualParent, FuncBinding, FuncBindings, FuncBindingsError, FuncBindingsResult, +}; + +pub struct AttributeBinding; + +impl AttributeBinding { + pub async fn find_eventual_parent( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult { + let eventual_parent = + AttributePrototype::eventual_parent(ctx, attribute_prototype_id).await?; + let parent = match eventual_parent { + AttributePrototypeEventualParent::Component(component_id, _) => { + EventualParent::Component(component_id) + } + AttributePrototypeEventualParent::SchemaVariantFromInputSocket( + schema_variant_id, + _, + ) => EventualParent::SchemaVariant(schema_variant_id), + + AttributePrototypeEventualParent::SchemaVariantFromOutputSocket( + schema_variant_id, + _, + ) => EventualParent::SchemaVariant(schema_variant_id), + AttributePrototypeEventualParent::SchemaVariantFromProp(schema_variant_id, _) => { + EventualParent::SchemaVariant(schema_variant_id) + } + }; + Ok(parent) + } + + pub(crate) async fn find_output_location( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult { + let eventual_parent = + AttributePrototype::eventual_parent(ctx, attribute_prototype_id).await?; + let output_location = match eventual_parent { + AttributePrototypeEventualParent::Component(_, attribute_value_id) => { + let prop_id = + AttributeValue::prop_id_for_id_or_error(ctx, attribute_value_id).await?; + AttributeFuncDestination::Prop(prop_id) + } + AttributePrototypeEventualParent::SchemaVariantFromOutputSocket( + _, + output_socket_id, + ) => AttributeFuncDestination::OutputSocket(output_socket_id), + AttributePrototypeEventualParent::SchemaVariantFromProp(_, prop_id) => { + AttributeFuncDestination::Prop(prop_id) + } + AttributePrototypeEventualParent::SchemaVariantFromInputSocket(_, _) => { + return Err(FuncBindingsError::MalformedInput("()".to_owned())); + } + }; + Ok(output_location) + } + + pub async fn assemble_eventual_parent( + ctx: &DalContext, + component_id: Option, + schema_variant_id: Option, + ) -> FuncBindingsResult> { + let eventual_parent = match (component_id, schema_variant_id) { + (None, None) => None, + (None, Some(schema_variant)) => { + Some(EventualParent::SchemaVariant(schema_variant.into())) + } + (Some(component_id), None) => Some(EventualParent::Component(component_id.into())), + (Some(component_id), Some(schema_variant)) => { + if Component::schema_variant_id(ctx, component_id.into()).await? + == schema_variant.into() + { + Some(EventualParent::SchemaVariant(schema_variant.into())) + } else { + return Err(FuncBindingsError::MalformedInput( + "component and schema variant mismatch".to_owned(), + )); + } + } + }; + Ok(eventual_parent) + } + pub fn assemble_attribute_output_location( + prop_id: Option, + output_socket_id: Option, + ) -> FuncBindingsResult { + let output_location = match (prop_id, output_socket_id) { + (None, Some(output_socket_id)) => { + AttributeFuncDestination::OutputSocket(output_socket_id.into()) + } + + (Some(prop_id), None) => AttributeFuncDestination::Prop(prop_id.into()), + _ => { + return Err(FuncBindingsError::MalformedInput( + "cannot set more than one output location".to_owned(), + )) + } + }; + Ok(output_location) + } + + pub(crate) async fn assemble_attribute_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + let mut bindings = vec![]; + for attribute_prototype_id in AttributePrototype::list_ids_for_func_id(ctx, func_id).await? + { + let eventual_parent = Self::find_eventual_parent(ctx, attribute_prototype_id).await?; + let output_location = Self::find_output_location(ctx, attribute_prototype_id).await?; + let attribute_prototype_argument_ids = + AttributePrototypeArgument::list_ids_for_prototype(ctx, attribute_prototype_id) + .await?; + + let mut argument_bindings = Vec::with_capacity(attribute_prototype_argument_ids.len()); + for attribute_prototype_argument_id in attribute_prototype_argument_ids { + argument_bindings.push( + AttributeArgumentBinding::assemble(ctx, attribute_prototype_argument_id) + .await?, + ); + } + + bindings.push(FuncBinding::Attribute { + func_id, + attribute_prototype_id, + eventual_parent, + output_location, + argument_bindings, + }); + } + Ok(bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.attribute.upsert_attribute_binding" + )] + /// For a given [`AttributeFuncOutputLocation`], remove the existing [`AttributePrototype`] + /// and arguments, then create a new one in it's place, with new arguments according to the + /// [`AttributeArgumentBinding`]s + /// Collect impacted AttributeValues along the way and enqueue them for DependentValuesUpdate + /// so the functions run upon being attached. + pub async fn upsert_attribute_binding( + ctx: &DalContext, + func_id: FuncId, + eventual_parent: Option, + output_location: AttributeFuncDestination, + prototype_arguments: Vec, + ) -> FuncBindingsResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + if func.kind != FuncKind::Attribute { + return Err(FuncBindingsError::UnexpectedFuncKind(func.kind)); + } + let attribute_prototype = AttributePrototype::new(ctx, func_id).await?; + let attribute_prototype_id = attribute_prototype.id; + let mut affected_attribute_value_ids = vec![]; + // if a parent was specified, use it. otherwise find the schema variant + // for the output location + let eventual_parent = match eventual_parent { + Some(eventual) => eventual, + None => EventualParent::SchemaVariant(output_location.find_schema_variant(ctx).await?), + }; + match output_location { + AttributeFuncDestination::Prop(prop_id) => { + match eventual_parent { + EventualParent::SchemaVariant(_) => { + if let Some(existing_prototype_id) = + AttributePrototype::find_for_prop(ctx, prop_id, &None).await? + { + // remove existing attribute prototype and arguments before we add the + // edge to the new one + + Self::delete_attribute_prototype_and_args(ctx, existing_prototype_id) + .await?; + } + Prop::add_edge_to_attribute_prototype( + ctx, + prop_id, + attribute_prototype.id, + EdgeWeightKind::Prototype(None), + ) + .await?; + } + EventualParent::Component(component_id) => { + let attribute_value_ids = + Prop::attribute_values_for_prop_id(ctx, prop_id).await?; + + for attribute_value_id in attribute_value_ids { + if component_id + == AttributeValue::component_id(ctx, attribute_value_id).await? + { + AttributeValue::set_component_prototype_id( + ctx, + attribute_value_id, + attribute_prototype.id, + None, + ) + .await?; + affected_attribute_value_ids.push(attribute_value_id); + } + } + } + } + } + AttributeFuncDestination::OutputSocket(output_socket_id) => { + // remove existing attribute prototype and arguments + match eventual_parent { + EventualParent::SchemaVariant(_) => { + if let Some(existing_proto) = + AttributePrototype::find_for_output_socket(ctx, output_socket_id) + .await? + { + Self::delete_attribute_prototype_and_args(ctx, existing_proto).await?; + } + OutputSocket::add_edge_to_attribute_prototype( + ctx, + output_socket_id, + attribute_prototype.id, + EdgeWeightKind::Prototype(None), + ) + .await?; + } + EventualParent::Component(component_id) => { + let attribute_value_ids = + OutputSocket::attribute_values_for_output_socket_id( + ctx, + output_socket_id, + ) + .await?; + for attribute_value_id in attribute_value_ids { + if component_id + == AttributeValue::component_id(ctx, attribute_value_id).await? + { + AttributeValue::set_component_prototype_id( + ctx, + attribute_value_id, + attribute_prototype.id, + None, + ) + .await?; + affected_attribute_value_ids.push(attribute_value_id); + } + } + } + } + } + } + if !affected_attribute_value_ids.is_empty() { + ctx.add_dependent_values_and_enqueue(affected_attribute_value_ids) + .await?; + } + for arg in &prototype_arguments { + // Ensure a func argument exists for each input location, before creating new Attribute Prototype Arguments + if let Err(err) = FuncArgument::get_by_id_or_error(ctx, arg.func_argument_id).await { + match err { + FuncArgumentError::WorkspaceSnapshot( + WorkspaceSnapshotError::WorkspaceSnapshotGraph( + WorkspaceSnapshotGraphError::NodeWithIdNotFound(raw_id), + ), + ) if raw_id == arg.func_argument_id.into() => continue, + err => return Err(err.into()), + } + } + + let attribute_prototype_argument = + AttributePrototypeArgument::new(ctx, attribute_prototype_id, arg.func_argument_id) + .await?; + match &arg.attribute_func_input_location { + super::AttributeFuncArgumentSource::Prop(prop_id) => { + attribute_prototype_argument + .set_value_from_prop_id(ctx, *prop_id) + .await? + } + super::AttributeFuncArgumentSource::InputSocket(input_socket_id) => { + attribute_prototype_argument + .set_value_from_input_socket_id(ctx, *input_socket_id) + .await? + } + // note: this isn't in use yet, but is ready for when we enable users to set default values via the UI + super::AttributeFuncArgumentSource::StaticArgument(value) => { + attribute_prototype_argument + .set_value_from_static_value( + ctx, + serde_json::from_str::(value.as_str())?, + ) + .await? + } + }; + } + let new_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.attribute.update_attribute_binding_arguments" + )] + /// For a given [`AttributePrototypeId`], remove the existing [`AttributePrototype`] + /// and arguments, then re-create them for the new inputs. + pub async fn update_attribute_binding_arguments( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + prototype_arguments: Vec, + ) -> FuncBindingsResult { + let func_id = AttributePrototype::func_id(ctx, attribute_prototype_id).await?; + //remove existing arguments first + Self::delete_attribute_prototype_args(ctx, attribute_prototype_id).await?; + + // recreate them + for arg in &prototype_arguments { + // Ensure the func argument exists before continuing. By continuing, we will not add the + // attribute prototype to the id set and will be deleted. + if let Err(err) = FuncArgument::get_by_id_or_error(ctx, arg.func_argument_id).await { + match err { + FuncArgumentError::WorkspaceSnapshot( + WorkspaceSnapshotError::WorkspaceSnapshotGraph( + WorkspaceSnapshotGraphError::NodeWithIdNotFound(raw_id), + ), + ) if raw_id == arg.func_argument_id.into() => continue, + err => return Err(err.into()), + } + } + + let attribute_prototype_argument = + AttributePrototypeArgument::new(ctx, attribute_prototype_id, arg.func_argument_id) + .await?; + match &arg.attribute_func_input_location { + super::AttributeFuncArgumentSource::Prop(prop_id) => { + attribute_prototype_argument + .set_value_from_prop_id(ctx, *prop_id) + .await? + } + super::AttributeFuncArgumentSource::InputSocket(input_socket_id) => { + attribute_prototype_argument + .set_value_from_input_socket_id(ctx, *input_socket_id) + .await? + } + super::AttributeFuncArgumentSource::StaticArgument(value) => { + attribute_prototype_argument + .set_value_from_static_value( + ctx, + serde_json::from_str::(value.as_str())?, + ) + .await? + } + }; + } + let new_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_bindings) + } + + pub(crate) async fn delete_attribute_prototype_and_args( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult<()> { + Self::delete_attribute_prototype_args(ctx, attribute_prototype_id).await?; + // should we fire a WsEvent here in case we just dropped an existing user authored + // attribute func? + AttributePrototype::remove(ctx, attribute_prototype_id).await?; + Ok(()) + } + async fn delete_attribute_prototype_args( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult<()> { + let current_attribute_prototype_arguments = + AttributePrototypeArgument::list_ids_for_prototype(ctx, attribute_prototype_id).await?; + for apa in current_attribute_prototype_arguments { + AttributePrototypeArgument::remove(ctx, apa).await?; + } + Ok(()) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.attribute.reset_attribute_binding" + )] + /// For a given [`AttributePrototypeId`], remove the existing [`AttributePrototype`] and [`AttributePrototypeArgument`]s + /// For a [`Component`], we'll reset the prototype to what is defined for the [`SchemaVariant`], and for now, reset the + /// [`SchemaVariant`]'s prototype to be the Identity Func. When the user regenerates the schema, we'll re-apply whatever has + /// been configured in the Schema Def function. This is a hold over until we remove this behavior from being configured in the + /// definition func and enable users to set intrinsic funcs via the UI. + pub async fn reset_attribute_binding( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult { + let func_id = AttributePrototype::func_id(ctx, attribute_prototype_id).await?; + + if let Some(attribute_value_id) = + AttributePrototype::attribute_value_id(ctx, attribute_prototype_id).await? + { + AttributeValue::use_default_prototype(ctx, attribute_value_id).await?; + } else { + // We're trying to reset the schema variant's prorotype, + // so set this prototype to be identity and remove all existing arguments. + // By setting to identity, this ensures that IF the user regenerates the schema variant def in the future, + // we'll correctly reset the value sources based on what's in that code + + let identity_func_id = Func::find_intrinsic(ctx, IntrinsicFunc::Identity).await?; + AttributePrototype::update_func_by_id(ctx, attribute_prototype_id, identity_func_id) + .await?; + // loop through and delete all existing attribute prototype arguments + let current_attribute_prototype_arguments = + AttributePrototypeArgument::list_ids_for_prototype(ctx, attribute_prototype_id) + .await?; + for apa in current_attribute_prototype_arguments { + AttributePrototypeArgument::remove(ctx, apa).await?; + } + } + let new_binding = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_binding) + } + + pub(crate) async fn compile_attribute_types( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult { + let mut input_ts_types = "type Input = {\n".to_string(); + + let mut output_ts_types = vec![]; + let mut argument_types = HashMap::new(); + let bindings = Self::assemble_attribute_bindings(ctx, func_id).await?; + for binding in bindings { + if let FuncBinding::Attribute { + output_location, + argument_bindings, + .. + } = binding + { + for arg in argument_bindings { + if let AttributeFuncArgumentSource::Prop(prop_id) = + arg.attribute_func_input_location + { + let prop = Prop::get_by_id_or_error(ctx, prop_id).await?; + let ts_type = prop.ts_type(ctx).await?; + + if let std::collections::hash_map::Entry::Vacant(e) = + argument_types.entry(arg.func_argument_id) + { + e.insert(vec![ts_type]); + } else if let Some(ts_types_for_arg) = + argument_types.get_mut(&arg.func_argument_id) + { + if !ts_types_for_arg.contains(&ts_type) { + ts_types_for_arg.push(ts_type) + } + } + } + let output_type = + if let AttributeFuncDestination::Prop(output_prop_id) = output_location { + Prop::get_by_id_or_error(ctx, output_prop_id) + .await? + .ts_type(ctx) + .await? + } else { + "any".to_string() + }; + if !output_ts_types.contains(&output_type) { + output_ts_types.push(output_type); + } + } + } + } + + for (arg_id, ts_types) in argument_types.iter() { + let func_arg = FuncArgument::get_by_id_or_error(ctx, *arg_id).await?; + let arg_name = func_arg.name; + input_ts_types + .push_str(format!("{}?: {} | null;\n", arg_name, ts_types.join(" | ")).as_str()); + } + input_ts_types.push_str("};"); + + let output_ts = format!("type Output = {};", output_ts_types.join(" | ")); + + Ok(format!("{}\n{}", input_ts_types, output_ts)) + } +} diff --git a/lib/dal/src/func/binding/attribute_argument.rs b/lib/dal/src/func/binding/attribute_argument.rs new file mode 100644 index 0000000000..be29f73738 --- /dev/null +++ b/lib/dal/src/func/binding/attribute_argument.rs @@ -0,0 +1,123 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + attribute::prototype::argument::{ + static_value::StaticArgumentValue, value_source::ValueSource, AttributePrototypeArgument, + AttributePrototypeArgumentId, + }, + func::argument::FuncArgumentId, + DalContext, InputSocketId, PropId, +}; + +use super::{FuncBindingsError, FuncBindingsResult}; + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] +pub enum AttributeFuncArgumentSource { + Prop(PropId), + InputSocket(InputSocketId), + StaticArgument(String), +} + +impl From for Option { + fn from(value: AttributeFuncArgumentSource) -> Self { + match value { + AttributeFuncArgumentSource::Prop(prop_id) => { + Some(::si_events::PropId::from_raw_id(prop_id.into())) + } + AttributeFuncArgumentSource::InputSocket(_) => None, + AttributeFuncArgumentSource::StaticArgument(_) => None, + } + } +} +impl From for Option { + fn from(value: AttributeFuncArgumentSource) -> Self { + match value { + AttributeFuncArgumentSource::Prop(_) => None, + AttributeFuncArgumentSource::InputSocket(input_socket_id) => Some( + ::si_events::InputSocketId::from_raw_id(input_socket_id.into()), + ), + AttributeFuncArgumentSource::StaticArgument(_) => None, + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct AttributeArgumentBinding { + pub func_argument_id: FuncArgumentId, + pub attribute_prototype_argument_id: Option, + pub attribute_func_input_location: AttributeFuncArgumentSource, +} +impl AttributeArgumentBinding { + pub fn into_frontend_type(&self) -> si_frontend_types::AttributeArgumentBinding { + si_frontend_types::AttributeArgumentBinding { + func_argument_id: self.func_argument_id.into(), + attribute_prototype_argument_id: self.attribute_prototype_argument_id.map(Into::into), + prop_id: self.attribute_func_input_location.clone().into(), + input_socket_id: self.attribute_func_input_location.clone().into(), + } + } + pub async fn assemble( + ctx: &DalContext, + attribute_prototype_argument_id: AttributePrototypeArgumentId, + ) -> FuncBindingsResult { + let attribute_prototype_argument = + AttributePrototypeArgument::get_by_id(ctx, attribute_prototype_argument_id).await?; + + let attribute_func_input_location = + match attribute_prototype_argument.value_source(ctx).await? { + Some(value_source) => match value_source { + ValueSource::InputSocket(input_socket_id) => { + AttributeFuncArgumentSource::InputSocket(input_socket_id) + } + ValueSource::Prop(prop_id) => AttributeFuncArgumentSource::Prop(prop_id), + ValueSource::StaticArgumentValue(static_argument_id) => { + let static_value = + StaticArgumentValue::get_by_id(ctx, static_argument_id).await?; + AttributeFuncArgumentSource::StaticArgument(static_value.value.to_string()) + } + value_source => { + return Err(FuncBindingsError::UnexpectedValueSource( + value_source, + attribute_prototype_argument_id, + )) + } + }, + None => { + return Err(FuncBindingsError::MissingValueSource( + attribute_prototype_argument_id, + )) + } + }; + + let func_argument_id = AttributePrototypeArgument::func_argument_id_by_id( + ctx, + attribute_prototype_argument_id, + ) + .await?; + + Ok(Self { + func_argument_id, + attribute_prototype_argument_id: Some(attribute_prototype_argument_id), + attribute_func_input_location, + }) + } + pub fn assemble_attribute_input_location( + prop_id: Option, + input_socket_id: Option, + ) -> FuncBindingsResult { + let input_location = match (prop_id, input_socket_id) { + (None, Some(input_socket_id)) => { + AttributeFuncArgumentSource::InputSocket(input_socket_id.into()) + } + + (Some(prop_id), None) => AttributeFuncArgumentSource::Prop(prop_id.into()), + _ => { + return Err(FuncBindingsError::MalformedInput( + "cannot set more than one output location".to_owned(), + )) + } + }; + Ok(input_location) + } +} diff --git a/lib/dal/src/func/binding/authentication.rs b/lib/dal/src/func/binding/authentication.rs new file mode 100644 index 0000000000..ef685a7b70 --- /dev/null +++ b/lib/dal/src/func/binding/authentication.rs @@ -0,0 +1,57 @@ +use telemetry::prelude::*; + +use crate::{DalContext, FuncId, SchemaVariant, SchemaVariantId}; + +use super::{FuncBinding, FuncBindings, FuncBindingsResult}; + +pub struct AuthBinding; + +impl AuthBinding { + pub(crate) async fn assemble_auth_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + let schema_variant_ids = SchemaVariant::list_for_auth_func(ctx, func_id).await?; + let mut bindings = vec![]; + for schema_variant_id in schema_variant_ids { + bindings.push(FuncBinding::Authentication { + schema_variant_id, + func_id, + }); + } + Ok(bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.authentication.create_auth_binding" + )] + /// Create an Auth Binding for a Schema Variant + pub async fn create_auth_binding( + ctx: &DalContext, + func_id: FuncId, + schema_variant_id: SchemaVariantId, + ) -> FuncBindingsResult { + SchemaVariant::new_authentication_prototype(ctx, func_id, schema_variant_id).await?; + let updated_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(updated_bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.authentication.create_auth_binding" + )] + /// Deletes an Auth Binding for a Schema Variant + pub async fn delete_auth_binding( + ctx: &DalContext, + func_id: FuncId, + schema_variant_id: SchemaVariantId, + ) -> FuncBindingsResult { + SchemaVariant::remove_authentication_prototype(ctx, func_id, schema_variant_id).await?; + let updated_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + + Ok(updated_bindings) + } +} diff --git a/lib/dal/src/func/binding/leaf.rs b/lib/dal/src/func/binding/leaf.rs new file mode 100644 index 0000000000..88df919ba6 --- /dev/null +++ b/lib/dal/src/func/binding/leaf.rs @@ -0,0 +1,277 @@ +use telemetry::prelude::*; + +use crate::{ + attribute::prototype::argument::AttributePrototypeArgument, + func::argument::FuncArgument, + prop::PropPath, + schema::variant::leaves::{LeafInput, LeafInputLocation, LeafKind}, + AttributePrototype, AttributePrototypeId, AttributeValue, Component, DalContext, Func, FuncId, + Prop, SchemaVariant, SchemaVariantId, +}; + +use super::{ + AttributeBinding, EventualParent, FuncBinding, FuncBindings, FuncBindingsError, + FuncBindingsResult, +}; + +pub struct LeafBinding; + +impl LeafBinding { + pub(crate) async fn assemble_code_gen_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + let inputs = Self::list_leaf_function_inputs(ctx, func_id).await?; + let mut bindings = vec![]; + let attribute_prototype_ids = + AttributePrototype::list_ids_for_func_id(ctx, func_id).await?; + + for attribute_prototype_id in attribute_prototype_ids { + let eventual_parent = + AttributeBinding::find_eventual_parent(ctx, attribute_prototype_id).await?; + + bindings.push(FuncBinding::CodeGeneration { + eventual_parent, + func_id, + inputs: inputs.clone(), + attribute_prototype_id, + }); + } + Ok(bindings) + } + + pub(crate) async fn assemble_qualification_bindings( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + let inputs = Self::list_leaf_function_inputs(ctx, func_id).await?; + let mut bindings = vec![]; + let attribute_prototype_ids = + AttributePrototype::list_ids_for_func_id(ctx, func_id).await?; + + for attribute_prototype_id in attribute_prototype_ids { + let eventual_parent = + AttributeBinding::find_eventual_parent(ctx, attribute_prototype_id).await?; + bindings.push(FuncBinding::Qualification { + eventual_parent, + func_id, + inputs: inputs.clone(), + attribute_prototype_id, + }); + } + Ok(bindings) + } + + async fn list_leaf_function_inputs( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult> { + Ok(FuncArgument::list_for_func(ctx, func_id) + .await? + .iter() + .filter_map(|arg| LeafInputLocation::maybe_from_arg_name(&arg.name)) + .collect()) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.leaf.create_leaf_func_binding" + )] + /// Create an Attribute Prototype for the given [`LeafKind`], with the provided input locations. + /// If no input locations are provided, default to [`LeafInputLocation::Domain`] + pub async fn create_leaf_func_binding( + ctx: &DalContext, + func_id: FuncId, + eventual_parent: EventualParent, + leaf_kind: LeafKind, + inputs: &[LeafInputLocation], + ) -> FuncBindingsResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + match eventual_parent { + EventualParent::SchemaVariant(schema_variant_id) => { + let inputs = match inputs.is_empty() { + true => &[LeafInputLocation::Domain], + false => inputs, + }; + SchemaVariant::upsert_leaf_function( + ctx, + schema_variant_id, + leaf_kind, + inputs, + &func, + ) + .await?; + } + EventualParent::Component(_) => { + //brit todo create this func + // let attribute_prototype_id = + // Component::upsert_leaf_function(ctx, component_id, leaf_kind, inputs, &func).await?; + } + } + + let new_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + Ok(new_bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.leaf.update_leaf_func_binding" + )] + /// Updates the inputs for the given [`LeafKind`], by deleting the existing prototype arguments + /// and creating new ones for the inputs provided + pub async fn update_leaf_func_binding( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + input_locations: &[LeafInputLocation], + ) -> FuncBindingsResult { + // find the prototype + let func_id = AttributePrototype::func_id(ctx, attribute_prototype_id).await?; + // update the input locations + let mut existing_args = FuncArgument::list_for_func(ctx, func_id).await?; + let mut inputs = vec![]; + for location in input_locations { + let arg_name = location.arg_name(); + let arg = match existing_args + .iter() + .find(|arg| arg.name.as_str() == arg_name) + { + Some(existing_arg) => existing_arg.clone(), + None => { + FuncArgument::new(ctx, arg_name, location.arg_kind(), None, func_id).await? + } + }; + + inputs.push(LeafInput { + location: *location, + func_argument_id: arg.id, + }); + } + + for existing_arg in existing_args.drain(..) { + if !inputs.iter().any( + |&LeafInput { + func_argument_id, .. + }| func_argument_id == existing_arg.id, + ) { + FuncArgument::remove(ctx, existing_arg.id).await?; + } + } + match AttributeBinding::find_eventual_parent(ctx, attribute_prototype_id).await? { + EventualParent::SchemaVariant(schema_variant_id) => { + SchemaVariant::upsert_leaf_function_inputs( + ctx, + inputs.as_slice(), + attribute_prototype_id, + schema_variant_id, + ) + .await?; + } + EventualParent::Component(_component_id) => { + // brit todo : write this func + // Component::upsert_leaf_function_inputs( + // ctx, + // inputs.as_slice(), + // attribute_prototype_id, + // component_id, + // ) + // .await?; + } + } + + let updated_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + + Ok(updated_bindings) + } + + #[instrument( + level = "info", + skip(ctx), + name = "func.binding.leaf.delete_leaf_func_binding" + )] + /// Deletes the attribute prototype for the given [`LeafKind`], including deleting the existing prototype arguments + /// and the created attribute value/prop beneath the Root Prop node for the [`LeafKind`] + pub async fn delete_leaf_func_binding( + ctx: &DalContext, + attribute_prototype_id: AttributePrototypeId, + ) -> FuncBindingsResult { + let func_id = AttributePrototype::func_id(ctx, attribute_prototype_id).await?; + let current_attribute_prototype_arguments = + AttributePrototypeArgument::list_ids_for_prototype(ctx, attribute_prototype_id).await?; + for apa in current_attribute_prototype_arguments { + AttributePrototypeArgument::remove(ctx, apa).await?; + } + if let Some(attribute_value_for_leaf_item) = + AttributePrototype::attribute_value_id(ctx, attribute_prototype_id).await? + { + AttributeValue::remove_by_id(ctx, attribute_value_for_leaf_item).await?; + AttributePrototype::remove(ctx, attribute_prototype_id).await?; + } else { + return Err(FuncBindingsError::FailedToRemoveLeafAttributeValue); + } + + let updated_bindings = FuncBindings::from_func_id(ctx, func_id).await?; + + Ok(updated_bindings) + + //brit todo delete the attribute value for the thing too + } + + pub(crate) async fn compile_leaf_func_types( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingsResult { + let attribute_prorotypes = AttributePrototype::list_ids_for_func_id(ctx, func_id).await?; + let mut schema_variant_ids = vec![]; + for attribute_prototype_id in attribute_prorotypes { + match AttributeBinding::find_eventual_parent(ctx, attribute_prototype_id).await? { + EventualParent::SchemaVariant(schema_variant_id) => { + schema_variant_ids.push(schema_variant_id) + } + EventualParent::Component(component_id) => { + // we probably want to grab the attribute value tree, but we'll defer to + // the prop tree for now + let schema_variant_id = Component::schema_variant_id(ctx, component_id).await?; + schema_variant_ids.push(schema_variant_id); + } + } + } + let mut ts_type = "type Input = {\n".to_string(); + let inputs = Self::list_leaf_function_inputs(ctx, func_id).await?; + for input_location in inputs { + let input_property = format!( + "{}?: {} | null;\n", + input_location.arg_name(), + Self::get_per_variant_types_for_prop_path( + ctx, + &schema_variant_ids, + &input_location.prop_path(), + ) + .await? + ); + ts_type.push_str(&input_property); + } + ts_type.push_str("};"); + + Ok(ts_type) + } + async fn get_per_variant_types_for_prop_path( + ctx: &DalContext, + variant_ids: &[SchemaVariantId], + path: &PropPath, + ) -> FuncBindingsResult { + let mut per_variant_types = vec![]; + + for variant_id in variant_ids { + let prop = Prop::find_prop_by_path(ctx, *variant_id, path).await?; + let ts_type = prop.ts_type(ctx).await?; + + if !per_variant_types.contains(&ts_type) { + per_variant_types.push(ts_type); + } + } + + Ok(per_variant_types.join(" | ")) + } +} diff --git a/lib/dal/src/prop.rs b/lib/dal/src/prop.rs index 1ffa7f7c71..be97fc7914 100644 --- a/lib/dal/src/prop.rs +++ b/lib/dal/src/prop.rs @@ -92,6 +92,18 @@ pub const SECRET_KIND_WIDGET_OPTION_LABEL: &str = "secretKind"; pk!(PropId); +impl From for PropId { + fn from(value: si_events::PropId) -> Self { + Self(value.into_raw_id()) + } +} + +impl From for si_events::PropId { + fn from(value: PropId) -> Self { + Self::from_raw_id(value.0) + } +} + // TODO: currently we only have string values in all widget_options but we should extend this to // support other types. However, we cannot use serde_json::Value since postcard will not // deserialize into a serde_json::Value. @@ -270,6 +282,20 @@ pub enum PropKind { String, } +impl From for si_frontend_types::PropKind { + fn from(value: PropKind) -> Self { + match value { + PropKind::Array => si_frontend_types::PropKind::Array, + PropKind::Boolean => si_frontend_types::PropKind::Boolean, + PropKind::Integer => si_frontend_types::PropKind::Integer, + PropKind::Json => si_frontend_types::PropKind::Json, + PropKind::Map => si_frontend_types::PropKind::Map, + PropKind::Object => si_frontend_types::PropKind::Object, + PropKind::String => si_frontend_types::PropKind::String, + } + } +} + impl PropKind { pub fn is_container(&self) -> bool { matches!(self, PropKind::Array | PropKind::Map | PropKind::Object) @@ -338,6 +364,25 @@ impl From for FuncBackendResponseType { } impl Prop { + pub async fn into_frontend_type(self, ctx: &DalContext) -> PropResult { + let path = self.path(ctx).await?.with_replaced_sep_and_prefix("/"); + Ok(si_frontend_types::Prop { + id: self.id().into(), + kind: self.kind.into(), + name: self.name.to_owned(), + path: path.to_owned(), + eligible_to_receive_data: { + // props can receive data if they're on a certain part of the prop tree + // or if they're not a child of an array/map (for now?) + let eligible_by_path = path == "/root/resource_value" + || path == "/root/si/color" + || path.starts_with("/root/domain/") + || path.starts_with("/root/resource_value/"); + eligible_by_path && self.can_be_used_as_prototype_arg + }, + eligible_to_send_data: self.can_be_used_as_prototype_arg, + }) + } pub fn assemble(prop_node_weight: PropNodeWeight, inner: PropContentV1) -> Self { Self { id: prop_node_weight.id().into(), diff --git a/lib/dal/src/schema/variant.rs b/lib/dal/src/schema/variant.rs index e48ab5fd06..5c6a5ec6ab 100644 --- a/lib/dal/src/schema/variant.rs +++ b/lib/dal/src/schema/variant.rs @@ -220,6 +220,12 @@ impl SchemaVariant { .collect(); let schema = Schema::get_by_id(ctx, schema_id).await?; + let props = Self::all_props(ctx, self.id()).await?; + let mut front_end_props = Vec::with_capacity(props.len()); + for prop in props { + let new_prop = prop.into_frontend_type(ctx).await?; + front_end_props.push(new_prop); + } Ok(frontend_types::SchemaVariant { schema_id: schema_id.into(), @@ -244,6 +250,7 @@ impl SchemaVariant { .collect(), is_locked: self.is_locked, timestamp: self.timestamp.into(), + props: front_end_props, }) } } @@ -1289,8 +1296,8 @@ impl SchemaVariant { Ok(attribute_prototype_id) } - /// This _private_ method upserts [inputs](LeafInput) to an _existing_ leaf function. - async fn upsert_leaf_function_inputs( + /// This method upserts [inputs](LeafInput) to an _existing_ leaf function. + pub async fn upsert_leaf_function_inputs( ctx: &DalContext, inputs: &[LeafInput], attribute_prototype_id: AttributePrototypeId, @@ -1828,7 +1835,7 @@ impl SchemaVariant { pub async fn list_for_action_func( ctx: &DalContext, func_id: FuncId, - ) -> SchemaVariantResult> { + ) -> SchemaVariantResult> { let workspace_snapshot = ctx.workspace_snapshot()?; // First, collect all the action prototypes using the func. @@ -1858,7 +1865,8 @@ impl SchemaVariant { if let Some(ContentAddressDiscriminants::SchemaVariant) = node_weight.content_address_discriminants() { - schema_variant_ids.push(node_weight.id().into()); + schema_variant_ids + .push((node_weight.id().into(), action_prototype_raw_id.into())); } } } diff --git a/lib/dal/src/schema/variant/authoring.rs b/lib/dal/src/schema/variant/authoring.rs index 6d4d9c0adc..449c5892cd 100644 --- a/lib/dal/src/schema/variant/authoring.rs +++ b/lib/dal/src/schema/variant/authoring.rs @@ -135,7 +135,7 @@ impl VariantAuthoringClient { .await?; let asset_func_spec = build_asset_func_spec(&asset_func)?; - let definition = execute_asset_func(ctx, &asset_func).await?; + let definition = Self::execute_asset_func(ctx, &asset_func).await?; let metadata = SchemaVariantMetadataJson { schema_name: name.clone(), @@ -192,7 +192,7 @@ impl VariantAuthoringClient { let cloned_func = old_func.duplicate(ctx, schema_name.clone()).await?; let cloned_func_spec = build_asset_func_spec(&cloned_func)?; - let definition = execute_asset_func(ctx, &cloned_func).await?; + let definition = Self::execute_asset_func(ctx, &cloned_func).await?; let metadata = SchemaVariantMetadataJson { schema_name: schema_name.clone(), @@ -316,7 +316,7 @@ impl VariantAuthoringClient { let schema_name = schema_name.into(); let asset_func_spec = build_asset_func_spec(&asset_func)?; - let definition = execute_asset_func(ctx, &asset_func).await?; + let definition = Self::execute_asset_func(ctx, &asset_func).await?; let metadata = SchemaVariantMetadataJson { schema_name: schema_name.clone(), version: SchemaVariant::generate_version_string(), @@ -444,7 +444,7 @@ impl VariantAuthoringClient { .await?; let asset_func_spec = build_asset_func_spec(&new_asset_func.clone())?; - let definition = execute_asset_func(ctx, &new_asset_func).await?; + let definition = Self::execute_asset_func(ctx, &new_asset_func).await?; let metadata = SchemaVariantMetadataJson { schema_name: schema_name.clone(), @@ -563,7 +563,7 @@ impl VariantAuthoringClient { // Create new schema variant based on the asset func let asset_func_spec = build_asset_func_spec(&unlocked_asset_func.clone())?; - let definition = execute_asset_func(ctx, &unlocked_asset_func).await?; + let definition = Self::execute_asset_func(ctx, &unlocked_asset_func).await?; let metadata = SchemaVariantMetadataJson { schema_name: schema.name.clone(), @@ -698,6 +698,48 @@ impl VariantAuthoringClient { .await?; Ok(()) } + pub async fn execute_asset_func( + ctx: &DalContext, + asset_func: &Func, + ) -> VariantAuthoringResult { + let result_channel = FuncRunner::run_asset_definition_func(ctx, asset_func).await?; + let func_run_value = result_channel + .await + .map_err(|_| VariantAuthoringError::FuncRunGone)??; + + if let Some(error) = func_run_value + .value() + .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? + .as_object() + .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? + .get("error") + .and_then(|e| e.as_str()) + { + return Err(VariantAuthoringError::FuncExecutionFailure( + error.to_owned(), + )); + } + let func_resp = func_run_value + .value() + .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? + .as_object() + .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? + .get("definition") + .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))?; + + ctx.layer_db() + .func_run() + .set_state_to_success( + func_run_value.func_run_id(), + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + Ok(serde_json::from_value::( + func_resp.to_owned(), + )?) + } } async fn build_variant_spec_based_on_existing_variant( @@ -760,49 +802,6 @@ fn build_asset_func_spec(asset_func: &Func) -> VariantAuthoringResult .build()?) } -async fn execute_asset_func( - ctx: &DalContext, - asset_func: &Func, -) -> VariantAuthoringResult { - let result_channel = FuncRunner::run_asset_definition_func(ctx, asset_func).await?; - let func_run_value = result_channel - .await - .map_err(|_| VariantAuthoringError::FuncRunGone)??; - - if let Some(error) = func_run_value - .value() - .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? - .as_object() - .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? - .get("error") - .and_then(|e| e.as_str()) - { - return Err(VariantAuthoringError::FuncExecutionFailure( - error.to_owned(), - )); - } - let func_resp = func_run_value - .value() - .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? - .as_object() - .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))? - .get("definition") - .ok_or(VariantAuthoringError::FuncExecution(asset_func.id))?; - - ctx.layer_db() - .func_run() - .set_state_to_success( - func_run_value.func_run_id(), - ctx.events_tenancy(), - ctx.events_actor(), - ) - .await?; - - Ok(serde_json::from_value::( - func_resp.to_owned(), - )?) -} - #[allow(clippy::result_large_err)] fn build_pkg_spec_for_variant( schema_name: &str, diff --git a/lib/dal/src/schema/variant/leaves.rs b/lib/dal/src/schema/variant/leaves.rs index 5ffdd6fbf3..22210344f7 100644 --- a/lib/dal/src/schema/variant/leaves.rs +++ b/lib/dal/src/schema/variant/leaves.rs @@ -55,6 +55,18 @@ impl From for PkgLeafKind { } } +impl From for si_frontend_types::LeafInputLocation { + fn from(value: LeafInputLocation) -> Self { + match value { + LeafInputLocation::Code => si_frontend_types::LeafInputLocation::Code, + LeafInputLocation::DeletedAt => si_frontend_types::LeafInputLocation::DeletedAt, + LeafInputLocation::Domain => si_frontend_types::LeafInputLocation::Domain, + LeafInputLocation::Resource => si_frontend_types::LeafInputLocation::Resource, + LeafInputLocation::Secrets => si_frontend_types::LeafInputLocation::Secrets, + } + } +} + /// This enum provides available child [`Prop`](crate::Prop) trees of [`RootProp`](crate::RootProp) /// that can be used as "inputs" for [`Funcs`](crate::Func) on leaves. /// @@ -62,7 +74,7 @@ impl From for PkgLeafKind { /// as "inputs" in order to prevent cycles. This enum provides an approved subset of those /// children_. #[remain::sorted] -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] pub enum LeafInputLocation { /// The input location corresponding to "/root/code". @@ -115,6 +127,17 @@ impl From for PkgLeafInputLocation { } } } +impl From for LeafInputLocation { + fn from(value: si_frontend_types::LeafInputLocation) -> Self { + match value { + si_frontend_types::LeafInputLocation::Code => LeafInputLocation::Code, + si_frontend_types::LeafInputLocation::DeletedAt => LeafInputLocation::DeletedAt, + si_frontend_types::LeafInputLocation::Domain => LeafInputLocation::Domain, + si_frontend_types::LeafInputLocation::Resource => LeafInputLocation::Resource, + si_frontend_types::LeafInputLocation::Secrets => LeafInputLocation::Secrets, + } + } +} impl From for LeafInputLocation { fn from(value: PkgLeafInputLocation) -> LeafInputLocation { diff --git a/lib/dal/src/socket/input.rs b/lib/dal/src/socket/input.rs index e718ca64d3..8d00c5e59c 100644 --- a/lib/dal/src/socket/input.rs +++ b/lib/dal/src/socket/input.rs @@ -505,6 +505,7 @@ impl From for frontend_types::InputSocket { Self { id: value.id.into(), name: value.name, + eligible_to_send_data: false, } } } diff --git a/lib/dal/src/socket/output.rs b/lib/dal/src/socket/output.rs index cdad65861e..fda9fc9118 100644 --- a/lib/dal/src/socket/output.rs +++ b/lib/dal/src/socket/output.rs @@ -495,6 +495,8 @@ impl From for frontend_types::OutputSocket { Self { id: value.id.into(), name: value.name, + //default to false, but figure out how to do this better + eligible_to_receive_data: false, } } } diff --git a/lib/dal/src/ws_event.rs b/lib/dal/src/ws_event.rs index 53ed273479..ebf743a6f3 100644 --- a/lib/dal/src/ws_event.rs +++ b/lib/dal/src/ws_event.rs @@ -15,8 +15,9 @@ use crate::component::{ ComponentUpdatedPayload, ComponentUpgradedPayload, ConnectionCreatedPayload, ConnectionDeletedPayload, InferredEdgeRemovePayload, InferredEdgeUpsertPayload, }; +use crate::func::binding::FuncBindingsWsEventPayload; use crate::func::runner::FuncRunLogUpdatedPayload; -use crate::func::FuncWsEventPayload; +use crate::func::{FuncWsEventCodeSaved, FuncWsEventFuncSummary, FuncWsEventPayload}; use crate::pkg::{ ImportWorkspaceVotePayload, WorkspaceActorPayload, WorkspaceImportApprovalActorPayload, }; @@ -87,9 +88,13 @@ pub enum WsPayload { ConnectionDeleted(ConnectionDeletedPayload), Cursor(CursorPayload), FuncArgumentsSaved(FuncWsEventPayload), + FuncBindingsUpdated(FuncBindingsWsEventPayload), + FuncCodeSaved(FuncWsEventCodeSaved), + FuncCreated(FuncWsEventFuncSummary), FuncDeleted(FuncWsEventPayload), FuncRunLogUpdated(FuncRunLogUpdatedPayload), FuncSaved(FuncWsEventPayload), + FuncUpdated(FuncWsEventFuncSummary), ImportWorkspaceVote(ImportWorkspaceVotePayload), InferredEdgeRemove(InferredEdgeRemovePayload), InferredEdgeUpsert(InferredEdgeUpsertPayload), diff --git a/lib/dal/tests/integration_test/func/authoring.rs b/lib/dal/tests/integration_test/func/authoring.rs index df427af2e2..90be7bd15c 100644 --- a/lib/dal/tests/integration_test/func/authoring.rs +++ b/lib/dal/tests/integration_test/func/authoring.rs @@ -1,3 +1,4 @@ +mod binding; mod create_func; mod func_argument; mod save_and_exec; diff --git a/lib/dal/tests/integration_test/func/authoring/binding.rs b/lib/dal/tests/integration_test/func/authoring/binding.rs new file mode 100644 index 0000000000..7f3d7bf388 --- /dev/null +++ b/lib/dal/tests/integration_test/func/authoring/binding.rs @@ -0,0 +1,3 @@ +mod action; +mod attribute; +mod authentication; diff --git a/lib/dal/tests/integration_test/func/authoring/binding/action.rs b/lib/dal/tests/integration_test/func/authoring/binding/action.rs new file mode 100644 index 0000000000..757bf0a349 --- /dev/null +++ b/lib/dal/tests/integration_test/func/authoring/binding/action.rs @@ -0,0 +1,328 @@ +use dal::action::prototype::{ActionKind, ActionPrototype}; +use dal::action::{Action, ActionId}; +use dal::func::authoring::FuncAuthoringClient; +use dal::func::binding::action::ActionBinding; +use dal::func::binding::FuncBindings; +use dal::func::summary::FuncSummary; +use dal::func::view::FuncView; +use dal::func::{FuncAssociations, FuncKind}; +use dal::{DalContext, Func, Schema, SchemaVariant}; +use dal_test::helpers::{create_component_for_schema_name, ChangeSetTestHelpers}; +use dal_test::test; + +#[test] +async fn attach_multiple_action_funcs(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "katy perry") + .await + .expect("unable to find by name") + .expect("no schema found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("unable to get default schema variant"); + + // Cache the total number of funcs before continuing. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + let total_funcs = funcs.len(); + + // Attach one action func to the schema variant and commit. + let func_id = Func::find_id_by_name(ctx, "test:createActionFallout") + .await + .expect("unable to find the func") + .expect("no func found"); + + ActionBinding::create_action_binding(ctx, func_id, ActionKind::Create, schema_variant_id) + .await + .expect("could not create action binding"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Attach a second action func to the same schema variant and commit. + let func_id = Func::find_id_by_name(ctx, "test:deleteActionSwifty") + .await + .expect("unable to find the func") + .expect("no func found"); + ActionBinding::create_action_binding(ctx, func_id, ActionKind::Destroy, schema_variant_id) + .await + .expect("could not create action binding"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Now, let's list all funcs and see the two that were attached. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + assert_eq!( + total_funcs + 2, // expected + funcs.len() // actual + ); +} + +#[test] +async fn error_when_attaching_an_exisiting_type(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "fallout") + .await + .expect("unable to find by name") + .expect("no schema found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("unable to get default schema variant"); + let func_id = Func::find_id_by_name(ctx, "test:createActionFallout") + .await + .expect("unable to find the func"); + assert!(func_id.is_some()); + + let new_action_func_name = "anotherCreate"; + FuncAuthoringClient::create_func( + ctx, + FuncKind::Action, + Some(new_action_func_name.to_string()), + None, + ) + .await + .expect("could not create func"); + + let func_id = Func::find_id_by_name(ctx, new_action_func_name) + .await + .expect("unable to find the func") + .expect("no func found"); + let func = Func::get_by_id_or_error(ctx, func_id) + .await + .expect("unable to get func by id"); + let func_view = FuncView::assemble(ctx, &func) + .await + .expect("unable to assemble a func view"); + let (func_view_kind, mut schema_variant_ids) = func_view + .associations + .expect("empty associations") + .get_action_internals() + .expect("could not get internals"); + schema_variant_ids.push(schema_variant_id); + assert!(FuncAuthoringClient::save_func( + ctx, + func_view.id, + func_view.display_name, + func_view.name, + func_view.description, + func_view.code, + Some(FuncAssociations::Action { + kind: func_view_kind, + schema_variant_ids, + }), + ) + .await + .is_err()); +} + +#[test] +async fn detach_attach_then_delete_action_func_while_enqueued(ctx: &mut DalContext) { + pub async fn can_assemble(ctx: &DalContext, action_id: ActionId) -> bool { + let action = Action::get_by_id(ctx, action_id) + .await + .expect("unable to get action"); + let prototype_id = Action::prototype_id(ctx, action_id) + .await + .expect("unable to get prototype id"); + let _prototype = ActionPrototype::get_by_id(ctx, prototype_id) + .await + .expect("unable to get prototype"); + let _func_run_id = ctx + .layer_db() + .func_run() + .get_last_run_for_action_id(ctx.events_tenancy().workspace_pk, action.id().into()) + .await + .expect("unable to get func run id") + .map(|f| f.id()); + let _component_id = Action::component_id(ctx, action_id) + .await + .expect("unable to get component id"); + let _my_dependencies = action + .get_all_dependencies(ctx) + .await + .expect("unable to get dependencies"); + let _dependent_on = Action::get_dependent_actions_by_id(ctx, action_id) + .await + .expect("unable to get dependent actions"); + let _hold_status_influenced_by = action + .get_hold_status_influenced_by(ctx) + .await + .expect("unable to get hold status"); + true + } + let schema = Schema::find_by_name(ctx, "starfield") + .await + .expect("unable to find by name") + .expect("no schema found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("unable to get default schema variant"); + + // Cache the total number of funcs before continuing. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + let initial_total_funcs = funcs.len(); + + // create a component + let new_component = create_component_for_schema_name(ctx, "starfield", "component") + .await + .expect("unable to create new component"); + + // check that the action func has been enqueued + let enqueued_actions = Action::list_topologically(ctx) + .await + .expect("can list actions"); + // create action views + + let mut queued = Vec::new(); + + for action_id in enqueued_actions.into_iter() { + if can_assemble(ctx, action_id).await { + queued.push(action_id); + } + } + // make sure there is one enqueued action + assert_eq!( + 1, //expected + queued.len() // actual + ); + let func_id = Func::find_id_by_name(ctx, "test:createActionStarfield") + .await + .expect("unable to find the func") + .expect("no func found"); + let _func = Func::get_by_id(ctx, func_id) + .await + .expect("unable to get func") + .expect("func is some"); + + // detach the action + for action_prototype_id in ActionPrototype::list_for_func_id(ctx, func_id) + .await + .expect("unable to list prototypes for func") + { + ActionBinding::delete_action_binding(ctx, action_prototype_id) + .await + .expect("could not delete action binding"); + } + + // check the func count for the schema variant is accurate + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + assert_eq!( + initial_total_funcs - 1, // expected + funcs.len() // actual + ); + + // check that the action has been removed from the queue + let enqueued_actions = Action::list_topologically(ctx) + .await + .expect("can list actions"); + let mut queued = Vec::new(); + + for action_id in enqueued_actions.into_iter() { + if can_assemble(ctx, action_id).await { + queued.push(action_id); + } + } + // make sure there aren't any enqueued actions + assert_eq!( + 0, //expected + queued.len() // actual + ); + + // reattach the create action, and enqueue it. All should work again + ActionBinding::create_action_binding(ctx, func_id, ActionKind::Create, schema_variant_id) + .await + .expect("could not create action binding"); + + // ensure it got reattached + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + let total_funcs = funcs.len(); + + assert_eq!( + initial_total_funcs, // expected + total_funcs, // actual + ); + + // manually enqueue the action + let mut action = None; + for prototype in ActionPrototype::for_variant(ctx, schema_variant_id) + .await + .expect("unable to list prototypes for variant") + { + if prototype.kind == ActionKind::Create { + action = Some( + Action::new(ctx, prototype.id, Some(new_component.id())) + .await + .expect("unable to upsert action"), + ); + break; + } + } + // ensure we enqueued the action + assert!(action.is_some()); + + let enqueued_actions = Action::list_topologically(ctx) + .await + .expect("can list actions"); + let mut queued = Vec::new(); + + for action_id in enqueued_actions.into_iter() { + if can_assemble(ctx, action_id).await { + queued.push(action_id); + } + } + assert_eq!( + 1, //expected + queued.len() // actual + ); + + // finally, delete the action + let func_id = Func::find_id_by_name(ctx, "test:createActionStarfield") + .await + .expect("unable to find the func") + .expect("no func found"); + + // sdf calls this first if there are associations, so call it here too + FuncBindings::delete_all_bindings_for_func_id(ctx, func_id) + .await + .expect("could not delete bindings"); + + Func::delete_by_id(ctx, func_id) + .await + .expect("unable to delete the func"); + + // check the func count for the schema variant + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + assert_eq!( + total_funcs - 1, // expected + funcs.len() // actual + ); + + // check that the action has been removed from the queue + let enqueued_actions = Action::list_topologically(ctx) + .await + .expect("can list actions"); + let mut queued = Vec::new(); + + for action_id in enqueued_actions.into_iter() { + if can_assemble(ctx, action_id).await { + queued.push(action_id); + } + } + // make sure there aren't any enqueued actions + assert_eq!( + 0, //expected + queued.len() // actual + ); +} diff --git a/lib/dal/tests/integration_test/func/authoring/binding/attribute.rs b/lib/dal/tests/integration_test/func/authoring/binding/attribute.rs new file mode 100644 index 0000000000..c91f30d246 --- /dev/null +++ b/lib/dal/tests/integration_test/func/authoring/binding/attribute.rs @@ -0,0 +1,269 @@ +use std::collections::HashSet; + +use crate::integration_test::func::authoring::save_func::save_func_setup; +use dal::attribute::prototype::argument::value_source::ValueSource; +use dal::attribute::prototype::argument::AttributePrototypeArgument; +use dal::func::argument::{FuncArgument, FuncArgumentKind}; +use dal::func::binding::attribute::AttributeBinding; +use dal::func::binding::{ + AttributeArgumentBinding, AttributeFuncDestination, AttributeFuncArgumentSource, EventualParent, + FuncBinding, FuncBindings, +}; +use dal::func::summary::FuncSummary; +use dal::prop::PropPath; +use dal::{ + AttributePrototype, AttributePrototypeId, DalContext, Func, Prop, Schema, SchemaVariant, +}; +use dal_test::helpers::ChangeSetTestHelpers; +use dal_test::test; +pub use si_frontend_types; + +#[test] +async fn create_attribute_prototype_with_attribute_prototype_argument(ctx: &mut DalContext) { + let (func_id, _) = save_func_setup(ctx, "test:falloutEntriesToGalaxies").await; + + // Ensure the prototypes look as we expect. + let bindings = FuncBindings::from_func_id(ctx, func_id) + .await + .expect("could not get bindings"); + + let attribute_prototype_ids = AttributePrototype::list_ids_for_func_id(ctx, func_id) + .await + .expect("could not list ids for func id"); + assert_eq!( + attribute_prototype_ids, // expected + bindings + .get_attribute_internals() + .expect("could not get attribute internals") + .iter() + .map(|v| v.0) + .collect::>() // actual + ); + + // Cache the variables we need. + let schema = Schema::find_by_name(ctx, "starfield") + .await + .expect("could not find schema") + .expect("schema not found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("no schema variant found"); + let input_location_prop_id = Prop::find_prop_id_by_path( + ctx, + schema_variant_id, + &PropPath::new(["root", "domain", "name"]), + ) + .await + .expect("could not find prop id by path"); + let output_location_prop_id = Prop::find_prop_id_by_path( + ctx, + schema_variant_id, + &PropPath::new(["root", "domain", "possible_world_b", "wormhole_1"]), + ) + .await + .expect("could not find prop id by path"); + + let attributes = bindings + .get_attribute_internals() + .expect("could not get attribute internals"); + + assert_eq!( + 1, // expected + attributes.len() // actual + ); + let existing_attribute_prototype_id = attributes.first().expect("empty attribute prototypes").0; + + // Add a new prototype with a new prototype argument. Use the existing func argument. + let func_argument = FuncArgument::find_by_name_for_func(ctx, "entries", func_id) + .await + .expect("could not perform find by name for func") + .expect("func argument not found"); + + let prototype_arguments = vec![AttributeArgumentBinding { + func_argument_id: func_argument.id, + attribute_prototype_argument_id: None, + attribute_func_input_location: AttributeFuncArgumentSource::Prop(input_location_prop_id), + }]; + + // create the new attribute prototype and commit + AttributeBinding::upsert_attribute_binding( + ctx, + func_id, + Some(EventualParent::SchemaVariant(schema_variant_id)), + AttributeFuncDestination::Prop(output_location_prop_id), + prototype_arguments, + ) + .await + .expect("could not create attribute prototype"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Ensure that everything looks as we expect with the new prototype and prototype argument. + let func = Func::get_by_id_or_error(ctx, func_id) + .await + .expect("could not get func by id"); + let func_summary = func + .into_frontend_type(ctx) + .await + .expect("could not get func summary"); + + assert_eq!( + func_id, // expected + func_summary.func_id.into() // actual + ); + let bindings = func_summary.bindings.bindings; + assert_eq!( + 2, // expected + bindings.len() // actual + ); + let mut arguments = FuncArgument::list_for_func(ctx, func_id) + .await + .expect("could not list func arguments"); + assert_eq!( + 1, // expected + arguments.len() // actual + ); + let attribute_prototype_ids = AttributePrototype::list_ids_for_func_id(ctx, func_id) + .await + .expect("could not find attribute prototype ids"); + assert_eq!( + 2, // expected + attribute_prototype_ids.len() // actual + ); + + // Gather up the expected bindings. + let mut expected: HashSet = HashSet::new(); + for attribute_prototype_id in attribute_prototype_ids { + let mut attribute_prototype_argument_ids = + AttributePrototypeArgument::list_ids_for_prototype(ctx, attribute_prototype_id) + .await + .expect("could not list ids for prototype"); + let attribute_prototype_argument_id = attribute_prototype_argument_ids + .pop() + .expect("empty attribute prototype argument ids"); + let value_source = + AttributePrototypeArgument::value_source_by_id(ctx, attribute_prototype_argument_id) + .await + .expect("could not get value source") + .expect("value source not found"); + + if existing_attribute_prototype_id == attribute_prototype_id { + // Assemble the expected bag for the existing prototype. + let input_socket_id = match value_source { + ValueSource::InputSocket(input_socket_id) => input_socket_id, + value_source => panic!("unexpected value source: {value_source:?}"), + }; + let existing_output_location_prop_id = Prop::find_prop_id_by_path( + ctx, + schema_variant_id, + &PropPath::new(["root", "domain", "universe", "galaxies"]), + ) + .await + .expect("could not find prop id by path"); + expected.insert( + FuncBinding::Attribute { + func_id, + attribute_prototype_id, + eventual_parent: EventualParent::SchemaVariant(schema_variant_id), + output_location: AttributeFuncDestination::Prop( + existing_output_location_prop_id, + ), + argument_bindings: vec![AttributeArgumentBinding { + func_argument_id: func_argument.id, + attribute_prototype_argument_id: Some(attribute_prototype_argument_id), + attribute_func_input_location: AttributeFuncArgumentSource::InputSocket( + input_socket_id, + ), + }], + } + .into(), + ); + } else { + // Assemble the expected bag for the new prototype. + let prop_id = match value_source { + ValueSource::Prop(prop_id) => prop_id, + value_source => panic!("unexpected value source: {value_source:?}"), + }; + assert_eq!( + input_location_prop_id, // expected + prop_id // actual + ); + expected.insert( + FuncBinding::Attribute { + func_id, + attribute_prototype_id, + eventual_parent: EventualParent::SchemaVariant(schema_variant_id), + output_location: AttributeFuncDestination::Prop(output_location_prop_id), + argument_bindings: vec![AttributeArgumentBinding { + func_argument_id: func_argument.id, + attribute_prototype_argument_id: Some(attribute_prototype_argument_id), + attribute_func_input_location: AttributeFuncArgumentSource::Prop(prop_id), + }], + } + .into(), + ); + } + } + + // Now that we have the expected prototypes, we can perform the final assertions. + assert_eq!( + expected, // expected + HashSet::from_iter(bindings.into_iter()), // actual + ); + let argument = arguments.pop().expect("empty func arguments"); + assert_eq!(func_argument.id, argument.id); + assert_eq!("entries", argument.name.as_str()); + assert_eq!(FuncArgumentKind::Array, argument.kind); + assert_eq!(Some(FuncArgumentKind::Object), argument.element_kind); +} + +#[test] +async fn detach_attribute_func(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "starfield") + .await + .expect("unable to find by name") + .expect("no schema found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("unable to get default schema variant"); + + // Cache the total number of funcs before continuing. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + let total_funcs = funcs.len(); + + // Detach one attribute func to the schema variant and commit. + let func_id = Func::find_id_by_name(ctx, "test:falloutEntriesToGalaxies") + .await + .expect("unable to find the func") + .expect("no func found"); + let attributes = FuncBindings::from_func_id(ctx, func_id) + .await + .expect("could not get bindings") + .get_attribute_internals() + .expect("could not get attribute internals"); + let prototype = attributes + .into_iter() + .find(|p| p.1 == EventualParent::SchemaVariant(schema_variant_id)) + .expect("could not find schema variant"); + AttributeBinding::reset_attribute_binding(ctx, prototype.0) + .await + .expect("could not reset prototype"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Now, let's list all funcs and see what's left. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + assert_eq!( + total_funcs - 1, // expected + funcs.len() // actual + ); + assert!(!funcs.iter().any(|summary| summary.id == func_id)); +} diff --git a/lib/dal/tests/integration_test/func/authoring/binding/authentication.rs b/lib/dal/tests/integration_test/func/authoring/binding/authentication.rs new file mode 100644 index 0000000000..482668341f --- /dev/null +++ b/lib/dal/tests/integration_test/func/authoring/binding/authentication.rs @@ -0,0 +1,74 @@ +use dal::func::authoring::FuncAuthoringClient; +use dal::func::binding::authentication::AuthBinding; +use dal::func::summary::FuncSummary; +use dal::func::FuncKind; +use dal::{DalContext, Func, Schema, SchemaVariant}; +use dal_test::helpers::ChangeSetTestHelpers; +use dal_test::test; + +#[test] +async fn attach_multiple_auth_funcs_with_creation(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "katy perry") + .await + .expect("unable to find by name") + .expect("no schema found"); + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()) + .await + .expect("unable to get default schema variant"); + + // Cache the total number of funcs before continuing. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + let total_funcs = funcs.len(); + + // Attach one auth func to the schema variant and commit. + let func_id = Func::find_id_by_name(ctx, "test:setDummySecretString") + .await + .expect("unable to find the func") + .expect("no func found"); + + AuthBinding::create_auth_binding(ctx, func_id, schema_variant_id) + .await + .expect("could not create auth binding"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Create an auth func to be attached and commit. + let new_auth_func_name = "shattered space"; + FuncAuthoringClient::create_func( + ctx, + FuncKind::Authentication, + Some(new_auth_func_name.to_string()), + None, + ) + .await + .expect("could not create func"); + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Attach a second auth func (the new one) to the same schema variant and commit. + let func_id = Func::find_id_by_name(ctx, new_auth_func_name) + .await + .expect("unable to find the func") + .expect("no func found"); + AuthBinding::create_auth_binding(ctx, func_id, schema_variant_id) + .await + .expect("could not create auth binding"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Now, let's list all funcs and see the two that were attached. + let funcs = FuncSummary::list_for_schema_variant_id(ctx, schema_variant_id) + .await + .expect("unable to get the funcs for a schema variant"); + assert_eq!( + total_funcs + 2, // expected + funcs.len() // actual + ); +} diff --git a/lib/dal/tests/integration_test/func/authoring/create_func.rs b/lib/dal/tests/integration_test/func/authoring/create_func.rs index d797280712..5465aa307b 100644 --- a/lib/dal/tests/integration_test/func/authoring/create_func.rs +++ b/lib/dal/tests/integration_test/func/authoring/create_func.rs @@ -3,9 +3,11 @@ use dal::diagram::Diagram; use dal::func::authoring::{ AttributeOutputLocation, CreateFuncOptions, FuncAuthoringClient, FuncAuthoringError, }; +use dal::func::binding::{AttributeFuncDestination, EventualParent}; use dal::func::FuncKind; use dal::prop::PropPath; use dal::schema::variant::authoring::VariantAuthoringClient; +use dal::schema::variant::leaves::LeafKind; use dal::{AttributeValue, ChangeSet, DalContext, Func, OutputSocket, Prop, Schema, SchemaVariant}; use dal_test::helpers::{create_component_for_schema_name, ChangeSetTestHelpers}; use dal_test::test; @@ -110,6 +112,62 @@ async fn create_qualification_with_schema_variant(ctx: &mut DalContext) { .expect("Unable to get a func"); assert!(head_func.is_none()); } +#[test] +async fn create_qualification_with_schema_variant_chainsaw(ctx: &mut DalContext) { + let maybe_swifty_schema = Schema::find_by_name(ctx, "swifty") + .await + .expect("unable to get schema"); + assert!(maybe_swifty_schema.is_some()); + + let swifty_schema = maybe_swifty_schema.unwrap(); + let maybe_sv_id = swifty_schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get schema variant"); + assert!(maybe_sv_id.is_some()); + let sv_id = maybe_sv_id.unwrap(); + + let func_name = "Paul's Test Func".to_string(); + let func = FuncAuthoringClient::create_new_leaf_func( + ctx, + Some(func_name.clone()), + LeafKind::Qualification, + EventualParent::SchemaVariant(sv_id), + &[], + ) + .await + .expect("unable to create func"); + + let schema_funcs = SchemaVariant::all_funcs(ctx, sv_id) + .await + .expect("Unable to get all schema variant funcs"); + + assert_eq!(FuncKind::Qualification, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!(Some("async function main(component: Input): Promise {\n return {\n result: 'success',\n message: 'Component qualified'\n };\n}\n".to_string()), func.code_plaintext().expect("has code")); + + let mut expected_func: Vec = schema_funcs + .into_iter() + .filter(|f| f.name == func_name) + .collect(); + assert!(!expected_func.is_empty()); + assert_eq!(func_name, expected_func.pop().unwrap().name); + + let head_change_set = ctx + .get_workspace_default_change_set_id() + .await + .expect("Unable to find HEAD changeset id"); + + ctx.update_visibility_and_snapshot_to_visibility(head_change_set) + .await + .expect("Unable to go back to HEAD"); + + let head_func = Func::find_id_by_name(ctx, func_name.clone()) + .await + .expect("Unable to get a func"); + assert!(head_func.is_none()); +} #[test] async fn create_codegen_no_options(ctx: &mut DalContext) { @@ -200,6 +258,63 @@ async fn create_codegen_with_schema_variant(ctx: &mut DalContext) { .expect("Unable to get a func"); assert!(head_func.is_none()); } +#[test] +async fn create_codegen_with_schema_variant_chainsaw(ctx: &mut DalContext) { + let maybe_swifty_schema = Schema::find_by_name(ctx, "swifty") + .await + .expect("unable to get schema"); + assert!(maybe_swifty_schema.is_some()); + + let swifty_schema = maybe_swifty_schema.unwrap(); + let maybe_sv_id = swifty_schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get schema variant"); + assert!(maybe_sv_id.is_some()); + let sv_id = maybe_sv_id.unwrap(); + + let func_name = "Paul's Test Func".to_string(); + + let func = FuncAuthoringClient::create_new_leaf_func( + ctx, + Some(func_name.clone()), + LeafKind::CodeGeneration, + EventualParent::SchemaVariant(sv_id), + &[], + ) + .await + .expect("unable to create func"); + + let schema_funcs = SchemaVariant::all_funcs(ctx, sv_id) + .await + .expect("Unable to get all schema variant funcs"); + + assert_eq!(FuncKind::CodeGeneration, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!(Some("async function main(component: Input): Promise {\n return {\n format: \"json\",\n code: JSON.stringify(component),\n };\n}\n".to_string()), func.code_plaintext().expect("has code")); + + let mut expected_func: Vec = schema_funcs + .into_iter() + .filter(|f| f.name == func_name) + .collect(); + assert!(!expected_func.is_empty()); + assert_eq!(func_name, expected_func.pop().unwrap().name); + + let head_change_set = ctx + .get_workspace_default_change_set_id() + .await + .expect("Unable to find HEAD changeset id"); + + ctx.update_visibility_and_snapshot_to_visibility(head_change_set) + .await + .expect("Unable to go back to HEAD"); + + let head_func = Func::find_id_by_name(ctx, func_name.clone()) + .await + .expect("Unable to get a func"); + assert!(head_func.is_none()); +} #[test] async fn create_attribute_no_options(ctx: &mut DalContext) { @@ -305,6 +420,77 @@ async fn create_attribute_override_dynamic_func_for_prop(ctx: &mut DalContext) { assert!(head_func.is_none()); } +#[test] +async fn create_attribute_override_dynamic_func_for_prop_chainsaw(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "swifty") + .await + .expect("unable to find schema by name") + .expect("schema not found"); + let schema_variant_id = schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get default schema variant id") + .expect("default schema variant id not found"); + let prop_id = Prop::find_prop_id_by_path( + ctx, + schema_variant_id, + &PropPath::new(["root", "domain", "name"]), + ) + .await + .expect("unable to get prop"); + + // Create the func and commit. + let func_name = "Paul's Test Func"; + let func = FuncAuthoringClient::create_new_attribute_func( + ctx, + Some(func_name.to_string()), + None, + AttributeFuncDestination::Prop(prop_id), + Vec::new(), + ) + .await + .expect("could not create func"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Ensure that the created func looks as expected. + assert_eq!(FuncKind::Attribute, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!( + Some( + "async function main(input: Input): Promise {\n return null;\n}\n".to_string() + ), + func.code_plaintext().expect("has code") + ); + + // Ensure the func is created and associated with the schema variant. + let funcs = SchemaVariant::all_funcs(ctx, schema_variant_id) + .await + .expect("Unable to get all schema variant funcs"); + let func = funcs + .iter() + .find(|f| f.name == func_name) + .expect("func not found") + .to_owned(); + assert_eq!(func_name, func.name); + + // Ensure that the func does not exist on head. + let head_change_set = ctx + .get_workspace_default_change_set_id() + .await + .expect("Unable to find HEAD changeset id"); + ctx.update_visibility_and_snapshot_to_visibility_no_editing_change_set(head_change_set) + .await + .expect("Unable to go back to HEAD"); + let head_func = Func::find_id_by_name(ctx, func_name) + .await + .expect("Unable to get a func"); + assert!(head_func.is_none()); +} + #[test] async fn create_attribute_override_dynamic_func_for_output_socket(ctx: &mut DalContext) { let schema = Schema::find_by_name(ctx, "swifty") @@ -375,6 +561,74 @@ async fn create_attribute_override_dynamic_func_for_output_socket(ctx: &mut DalC assert!(head_func.is_none()); } +#[test] +async fn create_attribute_override_dynamic_func_for_output_socket_chainsaw(ctx: &mut DalContext) { + let schema = Schema::find_by_name(ctx, "swifty") + .await + .expect("unable to find schema by name") + .expect("schema not found"); + let schema_variant_id = schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get default schema variant id") + .expect("default schema variant id not found"); + let output_socket = OutputSocket::find_with_name(ctx, "anything", schema_variant_id) + .await + .expect("could not perform find output socket") + .expect("output socket not found"); + + // Create the func and commit. + let func_name = "Paul's Test Func"; + + let func = FuncAuthoringClient::create_new_attribute_func( + ctx, + Some(func_name.to_string()), + None, + AttributeFuncDestination::OutputSocket(output_socket.id()), + vec![], + ) + .await + .expect("unable to create func"); + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // Ensure that the created func looks as expected. + assert_eq!(FuncKind::Attribute, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!( + Some( + "async function main(input: Input): Promise {\n return null;\n}\n".to_string() + ), + func.code_plaintext().expect("has code") + ); + + // Ensure the func is created and associated with the schema variant. + let funcs = SchemaVariant::all_funcs(ctx, schema_variant_id) + .await + .expect("Unable to get all schema variant funcs"); + let func = funcs + .iter() + .find(|f| f.name == func_name) + .expect("func not found") + .to_owned(); + assert_eq!(func_name, func.name); + + // Ensure that the func does not exist on head. + let head_change_set = ctx + .get_workspace_default_change_set_id() + .await + .expect("Unable to find HEAD changeset id"); + ctx.update_visibility_and_snapshot_to_visibility_no_editing_change_set(head_change_set) + .await + .expect("Unable to go back to HEAD"); + let head_func = Func::find_id_by_name(ctx, func_name) + .await + .expect("Unable to get a func"); + assert!(head_func.is_none()); +} + #[test] async fn create_action_no_options(ctx: &mut DalContext) { let func_name = "Paul's Test Action Func".to_string(); @@ -469,6 +723,66 @@ async fn create_action_with_schema_variant(ctx: &mut DalContext) { .expect("Unable to get a func"); assert!(head_func.is_none()); } +#[test] +async fn create_action_with_schema_variant_chainsaw(ctx: &mut DalContext) { + let maybe_swifty_schema = Schema::find_by_name(ctx, "small even lego") + .await + .expect("unable to get schema"); + assert!(maybe_swifty_schema.is_some()); + + let swifty_schema = maybe_swifty_schema.unwrap(); + let maybe_sv_id = swifty_schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get schema variant"); + assert!(maybe_sv_id.is_some()); + let sv_id = maybe_sv_id.unwrap(); + + let func_name = "Paul's Test Action Func".to_string(); + let func = FuncAuthoringClient::create_new_action_func( + ctx, + Some(func_name.clone()), + ActionKind::Update, + sv_id, + ) + .await + .expect("could not create action func"); + + let schema_funcs = SchemaVariant::all_funcs(ctx, sv_id) + .await + .expect("Unable to get all schema variant funcs"); + + assert_eq!(FuncKind::Action, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!( + Some("async function main(component: Input): Promise {\n throw new Error(\"unimplemented!\");\n}\n".to_string()), + func.code_plaintext().expect("has code") + ); + + let mut expected_func: Vec = schema_funcs + .into_iter() + .filter(|f| f.name == func_name) + .collect(); + assert!(!expected_func.is_empty()); + + let action_func = expected_func.pop().unwrap(); + assert_eq!(func_name, action_func.name); + + let head_change_set = ctx + .get_workspace_default_change_set_id() + .await + .expect("Unable to find HEAD changeset id"); + + ctx.update_visibility_and_snapshot_to_visibility(head_change_set) + .await + .expect("Unable to go back to HEAD"); + + let head_func = Func::find_id_by_name(ctx, func_name.clone()) + .await + .expect("Unable to get a func"); + assert!(head_func.is_none()); +} #[test] async fn duplicate_action_kinds_causes_error(ctx: &mut DalContext) { @@ -736,3 +1050,207 @@ async fn create_qualification_and_code_gen_with_existing_component(ctx: &mut Dal component_view ); } +#[test] +async fn create_qualification_and_code_gen_with_existing_component_chainsaw(ctx: &mut DalContext) { + let asset_name = "britsTestAsset".to_string(); + let description = None; + let link = None; + let category = "Integration Tests".to_string(); + let color = "#00b0b0".to_string(); + let variant_zero = VariantAuthoringClient::create_schema_and_variant( + ctx, + asset_name.clone(), + description.clone(), + link.clone(), + category.clone(), + color.clone(), + ) + .await + .expect("Unable to create new asset"); + + let my_asset_schema = variant_zero + .schema(ctx) + .await + .expect("Unable to get the schema for the variant"); + + let default_schema_variant = my_asset_schema + .get_default_schema_variant_id(ctx) + .await + .expect("unable to get the default schema variant id"); + assert!(default_schema_variant.is_some()); + assert_eq!(default_schema_variant, Some(variant_zero.id())); + + // Now let's update the variant + let first_code_update = "function main() {\n + const myProp = new PropBuilder().setName(\"testProp\").setKind(\"string\").build() + const myProp2 = new PropBuilder().setName(\"testPropWillRemove\").setKind(\"string\").build() + const arrayProp = new PropBuilder().setName(\"arrayProp\").setKind(\"array\").setEntry(\n + new PropBuilder().setName(\"arrayElem\").setKind(\"string\").build()\n + ).build();\n + return new AssetBuilder().addProp(myProp).addProp(arrayProp).build()\n}" + .to_string(); + + VariantAuthoringClient::save_variant_content( + ctx, + variant_zero.id(), + my_asset_schema.name.clone(), + variant_zero.display_name(), + variant_zero.category(), + variant_zero.description(), + variant_zero.link(), + variant_zero + .get_color(ctx) + .await + .expect("get color from schema variant"), + variant_zero.component_type(), + Some(first_code_update), + ) + .await + .expect("save variant contents"); + + let updated_variant_id = VariantAuthoringClient::regenerate_variant(ctx, variant_zero.id()) + .await + .expect("unable to update asset"); + + // We should still see that the schema variant we updated is the same as we have no components on the graph + assert_eq!(variant_zero.id(), updated_variant_id); + // Add a component to the diagram + let initial_component = + create_component_for_schema_name(ctx, my_asset_schema.name.clone(), "demo component") + .await + .expect("could not create component"); + let initial_diagram = Diagram::assemble(ctx) + .await + .expect("could not assemble diagram"); + assert_eq!(1, initial_diagram.components.len()); + + let domain_prop_av_id = initial_component + .domain_prop_attribute_value(ctx) + .await + .expect("able to get domain prop"); + + // Set the domain so we get some array elements + AttributeValue::update( + ctx, + domain_prop_av_id, + Some(serde_json::json!({ + "testProp": "test", + "testPropWillRemove": "testToBeRemoved", + "arrayProp": [ + "first", + "second" + ] + })), + ) + .await + .expect("update failed"); + + // Let's ensure that our prop is visible in the component + Prop::find_prop_id_by_path( + ctx, + updated_variant_id, + &PropPath::new(["root", "domain", "testProp"]), + ) + .await + .expect("able to find testProp prop"); + // now let's create a new code gen for the new schema variant + let func_name = "Code Gen Func".to_string(); + + let func = FuncAuthoringClient::create_new_leaf_func( + ctx, + Some(func_name.clone()), + LeafKind::CodeGeneration, + EventualParent::SchemaVariant(updated_variant_id), + &[], + ) + .await + .expect("could not create func"); + + let schema_funcs = SchemaVariant::all_funcs(ctx, updated_variant_id) + .await + .expect("Unable to get all schema variant funcs"); + + assert_eq!(FuncKind::CodeGeneration, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!(Some("async function main(component: Input): Promise {\n return {\n format: \"json\",\n code: JSON.stringify(component),\n };\n}\n".to_string()), + func.code_plaintext().expect("has code")); + + let mut expected_func: Vec = schema_funcs + .into_iter() + .filter(|f| f.name == func_name) + .collect(); + assert!(!expected_func.is_empty()); + assert_eq!(func_name, expected_func.pop().unwrap().name); + + // let's also create a new qualification fo the new schema variant + let func_name = "Qualification Func".to_string(); + let func = FuncAuthoringClient::create_new_leaf_func( + ctx, + Some(func_name.clone()), + LeafKind::Qualification, + EventualParent::SchemaVariant(updated_variant_id), + &[], + ) + .await + .expect("could not create func"); + + let schema_funcs = SchemaVariant::all_funcs(ctx, updated_variant_id) + .await + .expect("Unable to get all schema variant funcs"); + + assert_eq!(FuncKind::Qualification, func.kind); + assert_eq!(func_name, func.name); + assert_eq!(Some("main".to_string()), func.handler); + assert_eq!(Some("async function main(component: Input): Promise {\n return {\n result: 'success',\n message: 'Component qualified'\n };\n}\n".to_string()), func.code_plaintext().expect("has code")); + + let mut expected_func: Vec = schema_funcs + .into_iter() + .filter(|f| f.name == func_name) + .collect(); + assert!(!expected_func.is_empty()); + assert_eq!(func_name, expected_func.pop().unwrap().name); + + // commit changes, so DVU kicks off and we should see the outcome of the new qualification and code gen func + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + let component_view = initial_component + .view(ctx) + .await + .expect("get component view"); + + // This test confirms the code gen and qualification ran for the existing component with expected outputs + assert_eq!( + Some(serde_json::json!({ + "si": { + "name": "demo component", + "type": "component", + "color": "#00b0b0", + }, + "domain": { + "testProp": "test", + "arrayProp": [ + "first", + "second", + ] + }, + "resource_value": { + }, + "code": { + "Code Gen Func": { + "code": "{\"domain\":{\"testProp\":\"test\",\"arrayProp\":[\"first\",\"second\"]}}", + "format": + "json", + }, + }, + "qualification":{ + "Qualification Func": { + "result":"success", + "message":"Component qualified", + } + } + })), + component_view + ); +} diff --git a/lib/dal/tests/integration_test/func/authoring/save_func.rs b/lib/dal/tests/integration_test/func/authoring/save_func.rs index fe7420f0e0..b5527e7949 100644 --- a/lib/dal/tests/integration_test/func/authoring/save_func.rs +++ b/lib/dal/tests/integration_test/func/authoring/save_func.rs @@ -32,7 +32,10 @@ async fn qualification(ctx: &mut DalContext) { // Sets up the tests within the module. Find the func to be saved by name and then save it // immediately when found. This is the basic "does it work in place" check. -async fn save_func_setup(ctx: &mut DalContext, func_name: impl AsRef) -> (FuncId, FuncView) { +pub async fn save_func_setup( + ctx: &mut DalContext, + func_name: impl AsRef, +) -> (FuncId, FuncView) { let func_id = Func::find_id_by_name(ctx, func_name) .await .expect("could not perform find func by name") diff --git a/lib/sdf-server/src/server/service/v2.rs b/lib/sdf-server/src/server/service/v2.rs index 5d6e96ea0c..75ed837f8c 100644 --- a/lib/sdf-server/src/server/service/v2.rs +++ b/lib/sdf-server/src/server/service/v2.rs @@ -1,141 +1,20 @@ -use axum::{ - extract::{OriginalUri, Path}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::get, - Json, Router, -}; -use dal::{ChangeSetId, Schema, SchemaVariant, SchemaVariantId, WorkspacePk}; -use si_frontend_types as frontend_types; -use thiserror::Error; +use crate::server::state::AppState; +use axum::Router; -use crate::server::{ - extract::{AccessBuilder, HandlerContext, PosthogClient}, - state::AppState, - tracking::track, -}; - -use super::ApiError; +pub mod func; +pub mod variant; pub fn routes() -> Router { const PREFIX: &str = "/workspaces/:workspace_id/change-sets/:change_set_id"; - - Router::new() - .route( + let mut router: Router = Router::new(); + router = router + .nest( &format!("{PREFIX}/schema-variants"), - get(list_schema_variants), - ) - .route( - &format!("{PREFIX}/schema-variants/:schema_variant_id"), - get(get_variant), + crate::server::service::v2::variant::v2_routes(), ) -} - -pub async fn list_schema_variants( - HandlerContext(builder): HandlerContext, - AccessBuilder(access_builder): AccessBuilder, - PosthogClient(posthog_client): PosthogClient, - OriginalUri(original_uri): OriginalUri, - Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>, -) -> Result>, SchemaVariantsAPIError> { - let ctx = builder - .build(access_builder.build(change_set_id.into())) - .await?; - - let mut schema_variants = Vec::new(); - - // NOTE(victor): This is not optimized, since it loops twice through the defaults, but it'll get the job done for now - // determining the default should change soon, and then we can get rid of SchemaVariant::get_default_for_schema over here - for schema_id in Schema::list_ids(&ctx).await? { - let default_schema_variant = SchemaVariant::get_default_for_schema(&ctx, schema_id).await?; - if !default_schema_variant.ui_hidden() { - schema_variants.push( - default_schema_variant - .into_frontend_type(&ctx, schema_id) - .await?, - ) - } - - if let Some(unlocked) = SchemaVariant::get_unlocked_for_schema(&ctx, schema_id).await? { - if !unlocked.ui_hidden() { - schema_variants.push(unlocked.into_frontend_type(&ctx, schema_id).await?) - } - } - } - - track( - &posthog_client, - &ctx, - &original_uri, - "list_schema_variants", - serde_json::json!({}), - ); - - Ok(Json(schema_variants)) -} - -pub async fn get_variant( - HandlerContext(builder): HandlerContext, - AccessBuilder(access_builder): AccessBuilder, - PosthogClient(posthog_client): PosthogClient, - OriginalUri(original_uri): OriginalUri, - Path((_workspace_pk, change_set_id, schema_variant_id)): Path<( - WorkspacePk, - ChangeSetId, - SchemaVariantId, - )>, -) -> Result, SchemaVariantsAPIError> { - let ctx = builder - .build(access_builder.build(change_set_id.into())) - .await?; - - let schema_variant = SchemaVariant::get_by_id(&ctx, schema_variant_id).await?; - let schema_id = SchemaVariant::schema_id_for_schema_variant_id(&ctx, schema_variant_id).await?; - let schema_variant = schema_variant.into_frontend_type(&ctx, schema_id).await?; - - // Ported from `lib/sdf-server/src/server/service/variant/get_variant.rs`, so changes may be - // desired here... - - track( - &posthog_client, - &ctx, - &original_uri, - "get_variant", - serde_json::json!({ - "schema_name": &schema_variant.schema_name, - "variant_category": &schema_variant.category, - "variant_menu_name": schema_variant.display_name, - "variant_id": schema_variant.schema_variant_id, - "schema_id": schema_variant.schema_id, - "variant_component_type": schema_variant.component_type, - }), - ); - - Ok(Json(schema_variant)) -} - -#[remain::sorted] -#[derive(Debug, Error)] -pub enum SchemaVariantsAPIError { - #[error("schema error: {0}")] - Schema(#[from] dal::SchemaError), - #[error("schema error: {0}")] - SchemaVariant(#[from] dal::SchemaVariantError), - #[error("transactions error: {0}")] - Transactions(#[from] dal::TransactionsError), -} - -impl IntoResponse for SchemaVariantsAPIError { - fn into_response(self) -> Response { - let status_code = match &self { - Self::Transactions(dal::TransactionsError::BadWorkspaceAndChangeSet) => { - StatusCode::FORBIDDEN - } - // When a graph node cannot be found for a schema variant, it is not found - Self::SchemaVariant(dal::SchemaVariantError::NotFound(_)) => StatusCode::NOT_FOUND, - _ => ApiError::DEFAULT_ERROR_STATUS_CODE, - }; - - ApiError::new(status_code, self).into_response() - } + .nest( + &format!("{PREFIX}/funcs"), + crate::server::service::v2::func::v2_routes(), + ); + router } diff --git a/lib/sdf-server/src/server/service/v2/func.rs b/lib/sdf-server/src/server/service/v2/func.rs new file mode 100644 index 0000000000..15ea312c31 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func.rs @@ -0,0 +1,173 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Router, +}; +use dal::{ + func::{ + argument::FuncArgumentError, + authoring::{FuncAuthoringClient, FuncAuthoringError}, + binding::FuncBindingsError, + }, + ChangeSetError, DalContext, Func, FuncError, FuncId, WsEventError, +}; +use si_frontend_types::FuncCode; +use thiserror::Error; + +use crate::{server::state::AppState, service::ApiError}; + +pub mod argument; +pub mod binding; +pub mod create_func; +pub mod execute_func; +pub mod get_code; +pub mod list_funcs; +pub mod save_code; +pub mod test_execute; +pub mod update_func; + +#[remain::sorted] +#[derive(Debug, Error)] +pub enum FuncAPIError { + #[error("change set error: {0}")] + ChangeSet(#[from] ChangeSetError), + #[error("func error: {0}")] + Func(#[from] FuncError), + #[error("func argument error: {0}")] + FuncArgument(#[from] FuncArgumentError), + #[error("func authoring error: {0}")] + FuncAuthoring(#[from] FuncAuthoringError), + #[error("func bindings error: {0}")] + FuncBindings(#[from] FuncBindingsError), + #[error("The function name \"{0}\" is reserved")] + FuncNameReserved(String), + #[error("hyper error: {0}")] + Http(#[from] axum::http::Error), + #[error("missing action kind")] + MissingActionKindForActionFunc, + #[error("missing action prototype")] + MissingActionPrototype, + #[error("missing func id")] + MissingFuncId, + #[error("no input location given")] + MissingInputLocationForAttributeFunc, + #[error("no output location given")] + MissingOutputLocationForAttributeFunc, + #[error("missing prototype id")] + MissingPrototypeId, + #[error("missing schema varianta and func id for leaf func")] + MissingSchemaVariantAndFunc, + #[error("schema error: {0}")] + Schema(#[from] dal::SchemaError), + #[error("schema error: {0}")] + SchemaVariant(#[from] dal::SchemaVariantError), + #[error("serde json error: {0}")] + Serde(#[from] serde_json::Error), + #[error("transactions error: {0}")] + Transactions(#[from] dal::TransactionsError), + #[error("wrong function kind for binding")] + WrongFunctionKindForBinding, + #[error("ws event error: {0}")] + WsEvent(#[from] WsEventError), +} +pub type FuncAPIResult = std::result::Result; + +impl IntoResponse for FuncAPIError { + fn into_response(self) -> Response { + let status_code = match &self { + Self::Transactions(dal::TransactionsError::BadWorkspaceAndChangeSet) => { + StatusCode::FORBIDDEN + } + // these errors represent problems with the shape of the request + Self::MissingActionKindForActionFunc + | Self::MissingActionPrototype + | Self::MissingFuncId + | Self::MissingInputLocationForAttributeFunc + | Self::MissingOutputLocationForAttributeFunc + | Self::MissingPrototypeId + | Self::MissingSchemaVariantAndFunc => StatusCode::BAD_REQUEST, + // When a graph node cannot be found for a schema variant, it is not found + Self::SchemaVariant(dal::SchemaVariantError::NotFound(_)) => StatusCode::NOT_FOUND, + _ => ApiError::DEFAULT_ERROR_STATUS_CODE, + }; + + ApiError::new(status_code, self).into_response() + } +} + +pub fn v2_routes() -> Router { + Router::new() + // Func Stuff + .route("/", get(list_funcs::list_funcs)) + .route("/code", get(get_code::get_code)) // accepts a list of func_ids + .route("/create", post(create_func::create_func)) + .route("/:func_id/update", post(update_func::update_func)) // only save the func's metadata + .route("/:func_id/save_code", post(save_code::save_code)) // only saves func code + .route("/:func_id/test_execute", post(test_execute::test_execute)) + .route("/:func_id/execute", post(execute_func::execute_func)) + // Func Bindings + .route( + "/:func_id/bindings/create", + post(binding::create_binding::create_binding), + ) + .route( + "/:func_id/bindings/delete", + post(binding::delete_binding::delete_binding), + ) + .route( + "/:func_id/bindings/update", + post(binding::update_binding::update_binding), + ) + // Attribute Bindings + .route( + "/:func_id/create_attribute_binding", + post(binding::attribute::create_attribute_binding::create_attribute_binding), + ) + .route( + "/:func_id/reset_attribute_binding", + post(binding::attribute::reset_attribute_binding::reset_attribute_binding), + ) + .route( + "/:func_id/update_attribute_binding", + post(binding::attribute::update_attribute_binding::update_attribute_binding), + ) + // Func Arguments + .route( + "/:func_id/:func_argument_id/update", + post(argument::update_argument::update_func_argument), + ) + .route( + "/:func_id/create_argument", + post(argument::create_argument::create_func_argument), + ) + .route( + "/:func_id/:func_argument_id/delete", + post(argument::delete_argument::delete_func_argument), + ) +} + +// helper to assemble the front end struct to return the code and types so SDF can decide when these events need to fire +pub async fn get_code_response(ctx: &DalContext, func_id: FuncId) -> FuncAPIResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + let code = func.code_plaintext()?.unwrap_or("".to_string()); + Ok(FuncCode { + func_id: func.id.into(), + code: code.clone(), + types: get_types(ctx, func_id).await?, + }) +} + +// helper to get updated types to fire WSEvents so SDF can decide when these events need to fire +pub async fn get_types(ctx: &DalContext, func_id: FuncId) -> FuncAPIResult { + let func = Func::get_by_id_or_error(ctx, func_id).await?; + let types = [ + FuncAuthoringClient::compile_return_types(func.backend_response_type, func.backend_kind), + FuncAuthoringClient::compile_types_from_bindings(ctx, func_id) + .await? + .as_str(), + FuncAuthoringClient::compile_langjs_types(), + ] + .join("\n"); + Ok(types) +} diff --git a/lib/sdf-server/src/server/service/v2/func/argument.rs b/lib/sdf-server/src/server/service/v2/func/argument.rs new file mode 100644 index 0000000000..c3b085efc7 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/argument.rs @@ -0,0 +1,3 @@ +pub mod create_argument; +pub mod delete_argument; +pub mod update_argument; diff --git a/lib/sdf-server/src/server/service/v2/func/argument/create_argument.rs b/lib/sdf-server/src/server/service/v2/func/argument/create_argument.rs new file mode 100644 index 0000000000..bb298c3d00 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/argument/create_argument.rs @@ -0,0 +1,73 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::authoring::FuncAuthoringClient, ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, + WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIResult}, +}; + +pub async fn create_func_argument( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + FuncAuthoringClient::create_func_argument( + &ctx, + func_id, + request.name, + request.kind.into(), + request.element_kind.map(Into::into), + ) + .await?; + let types = get_types(&ctx, func_id).await?; + + let func_summary = Func::get_by_id_or_error(&ctx, func_id) + .await? + .into_frontend_type(&ctx) + .await?; + WsEvent::func_updated(&ctx, func_summary.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "create_func_argument", + serde_json::json!({ + "how": "/func/create_func_argument", + "func_id": func_id, + "func_name": func_summary.name.clone(), + "func_kind": func_summary.kind.clone(), + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&func_summary)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/argument/delete_argument.rs b/lib/sdf-server/src/server/service/v2/func/argument/delete_argument.rs new file mode 100644 index 0000000000..8fe699de89 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/argument/delete_argument.rs @@ -0,0 +1,67 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, +}; +use dal::{ + func::{argument::FuncArgumentId, authoring::FuncAuthoringClient}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIResult}, +}; + +pub async fn delete_func_argument( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id, func_argument_id)): Path<( + WorkspacePk, + ChangeSetId, + FuncId, + FuncArgumentId, + )>, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + FuncAuthoringClient::delete_func_argument(&ctx, func_argument_id).await?; + let types = get_types(&ctx, func_id).await?; + + let func_summary = Func::get_by_id_or_error(&ctx, func_id) + .await? + .into_frontend_type(&ctx) + .await?; + WsEvent::func_updated(&ctx, func_summary.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "delete_func_argument", + serde_json::json!({ + "how": "/func/delete_func_argument", + "func_id": func_id, + "func_name": func_summary.name.clone(), + "func_kind": func_summary.kind.clone(), + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&func_summary)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/argument/update_argument.rs b/lib/sdf-server/src/server/service/v2/func/argument/update_argument.rs new file mode 100644 index 0000000000..967d84a3b9 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/argument/update_argument.rs @@ -0,0 +1,77 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::argument::{FuncArgument, FuncArgumentId}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIResult}, +}; + +pub async fn update_func_argument( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id, func_argument_id)): Path<( + WorkspacePk, + ChangeSetId, + FuncId, + FuncArgumentId, + )>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + FuncArgument::modify_by_id(&ctx, func_argument_id, |existing_arg| { + existing_arg.name = request.name; + existing_arg.kind = request.kind.into(); + existing_arg.element_kind = request.element_kind.map(Into::into); + Ok(()) + }) + .await?; + let types = get_types(&ctx, func_id).await?; + + let func_summary = Func::get_by_id_or_error(&ctx, func_id) + .await? + .into_frontend_type(&ctx) + .await?; + WsEvent::func_updated(&ctx, func_summary.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "update_func_argument", + serde_json::json!({ + "how": "/func/update_func_argument", + "func_id": func_id, + "func_name": func_summary.name.clone(), + "func_kind": func_summary.kind.clone(), + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&func_summary)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding.rs b/lib/sdf-server/src/server/service/v2/func/binding.rs new file mode 100644 index 0000000000..e87729494c --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding.rs @@ -0,0 +1,4 @@ +pub mod attribute; +pub mod create_binding; +pub mod delete_binding; +pub mod update_binding; diff --git a/lib/sdf-server/src/server/service/v2/func/binding/attribute.rs b/lib/sdf-server/src/server/service/v2/func/binding/attribute.rs new file mode 100644 index 0000000000..0e4133c3d1 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/attribute.rs @@ -0,0 +1,3 @@ +pub mod create_attribute_binding; +pub mod reset_attribute_binding; +pub mod update_attribute_binding; diff --git a/lib/sdf-server/src/server/service/v2/func/binding/attribute/create_attribute_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/attribute/create_attribute_binding.rs new file mode 100644 index 0000000000..d172f86326 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/attribute/create_attribute_binding.rs @@ -0,0 +1,126 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{attribute::AttributeBinding, AttributeArgumentBinding, FuncBindings}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types::{self as frontend_types}; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn create_attribute_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Attribute => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Attribute { + func_id, + component_id, + schema_variant_id, + prop_id, + output_socket_id, + argument_bindings, + .. + } = binding + { + match func_id { + Some(func_id) => { + let eventual_parent = AttributeBinding::assemble_eventual_parent( + &ctx, + component_id, + schema_variant_id, + ) + .await?; + let attribute_output_location = + AttributeBinding::assemble_attribute_output_location( + prop_id, + output_socket_id, + )?; + let mut arguments: Vec = vec![]; + for arg_binding in argument_bindings { + let input_location = + AttributeArgumentBinding::assemble_attribute_input_location( + arg_binding.prop_id, + arg_binding.input_socket_id, + )?; + arguments.push(AttributeArgumentBinding { + func_argument_id: arg_binding + .func_argument_id + .into_raw_id() + .into(), + attribute_func_input_location: input_location, + attribute_prototype_argument_id: None, // when creating a new prototype, + // we don't have the attribute prototype arguments yet + }); + } + + AttributeBinding::upsert_attribute_binding( + &ctx, + func_id.into_raw_id().into(), + eventual_parent, + attribute_output_location, + arguments, + ) + .await?; + } + None => { + return Err(FuncAPIError::MissingFuncId); + } + } + } + } + } + _ => { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + }; + track( + &posthog_client, + &ctx, + &original_uri, + "created_attribute_binding", + serde_json::json!({ + "how": "/func/created_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + let types = get_types(&ctx, func_id).await?; + + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding/attribute/reset_attribute_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/attribute/reset_attribute_binding.rs new file mode 100644 index 0000000000..777ecc853d --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/attribute/reset_attribute_binding.rs @@ -0,0 +1,86 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{attribute::AttributeBinding, FuncBindings}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn reset_attribute_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Attribute => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Attribute { + attribute_prototype_id, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + AttributeBinding::reset_attribute_binding( + &ctx, + attribute_prototype_id.into_raw_id().into(), + ) + .await?; + } + None => return Err(FuncAPIError::MissingPrototypeId), + } + } + } + } + _ => { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + }; + track( + &posthog_client, + &ctx, + &original_uri, + "reset_attribute_binding", + serde_json::json!({ + "how": "/func/reset_attribute_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + let types = get_types(&ctx, func_id).await?; + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding/attribute/update_attribute_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/attribute/update_attribute_binding.rs new file mode 100644 index 0000000000..1c95be3e50 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/attribute/update_attribute_binding.rs @@ -0,0 +1,106 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{attribute::AttributeBinding, AttributeArgumentBinding, FuncBindings}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types::{self as frontend_types}; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn update_attribute_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Attribute => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Attribute { + argument_bindings, + attribute_prototype_id, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + let mut arguments: Vec = vec![]; + for arg_binding in argument_bindings { + let input_location = + AttributeArgumentBinding::assemble_attribute_input_location( + arg_binding.prop_id, + arg_binding.input_socket_id, + )?; + arguments.push(AttributeArgumentBinding { + func_argument_id: arg_binding + .func_argument_id + .into_raw_id() + .into(), + attribute_func_input_location: input_location, + attribute_prototype_argument_id: None, // when creating a new prototype, + // we don't have the attribute prototype arguments yet + }); + } + + AttributeBinding::update_attribute_binding_arguments( + &ctx, + attribute_prototype_id.into_raw_id().into(), + arguments, + ) + .await?; + } + None => return Err(FuncAPIError::MissingPrototypeId), + } + } + } + } + _ => { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + }; + track( + &posthog_client, + &ctx, + &original_uri, + "update_attribute_binding", + serde_json::json!({ + "how": "/func/update_attribute_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + let types = get_types(&ctx, func_id).await?; + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding/create_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/create_binding.rs new file mode 100644 index 0000000000..0675e897c5 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/create_binding.rs @@ -0,0 +1,155 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{action::ActionBinding, leaf::LeafBinding, EventualParent, FuncBindings}, + schema::variant::leaves::{LeafInputLocation, LeafKind}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn create_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Action => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Action { + schema_variant_id, + func_id, + kind, + .. + } = binding + { + match (kind, func_id, schema_variant_id) { + (Some(action_kind), Some(func_id), Some(schema_variant_id)) => { + ActionBinding::create_action_binding( + &ctx, + func_id.into(), + action_kind.into(), + schema_variant_id.into(), + ) + .await?; + } + _ => { + return Err(FuncAPIError::MissingActionKindForActionFunc); + } + } + } else { + return Err(FuncAPIError::MissingActionKindForActionFunc); + } + } + } + dal::func::FuncKind::CodeGeneration | dal::func::FuncKind::Qualification => { + for binding in request.bindings { + if let frontend_types::FuncBinding::CodeGeneration { + inputs, + schema_variant_id, + func_id, + .. + } = binding + { + match (schema_variant_id, func_id) { + (Some(schema_variant_id), Some(func_id)) => { + let inputs: Vec = + inputs.into_iter().map(|input| input.into()).collect(); + LeafBinding::create_leaf_func_binding( + &ctx, + func_id.into(), + EventualParent::SchemaVariant(schema_variant_id.into()), + LeafKind::CodeGeneration, + &inputs, + ) + .await?; + } + _ => { + return Err(FuncAPIError::MissingSchemaVariantAndFunc); + } + } + } else if let frontend_types::FuncBinding::Qualification { + inputs, + schema_variant_id, + func_id, + .. + } = binding + { + match (schema_variant_id, func_id) { + (Some(schema_variant_id), Some(func_id)) => { + let inputs: Vec = + inputs.into_iter().map(|input| input.into()).collect(); + LeafBinding::create_leaf_func_binding( + &ctx, + func_id.into(), + EventualParent::SchemaVariant(schema_variant_id.into()), + LeafKind::Qualification, + &inputs, + ) + .await?; + } + _ => { + return Err(FuncAPIError::MissingSchemaVariantAndFunc); + } + } + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + } + _ => { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + }; + track( + &posthog_client, + &ctx, + &original_uri, + "created_binding", + serde_json::json!({ + "how": "/func/created_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + + let types = get_types(&ctx, func_id).await?; + + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding/delete_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/delete_binding.rs new file mode 100644 index 0000000000..3199fce57d --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/delete_binding.rs @@ -0,0 +1,124 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{action::ActionBinding, leaf::LeafBinding, FuncBindings}, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn delete_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Action => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Action { + action_prototype_id: Some(action_prototype_id), + .. + } = binding + { + ActionBinding::delete_action_binding( + &ctx, + action_prototype_id.into_raw_id().into(), + ) + .await?; + } else { + return Err(FuncAPIError::MissingActionPrototype); + } + } + } + dal::func::FuncKind::CodeGeneration | dal::func::FuncKind::Qualification => { + for binding in request.bindings { + if let frontend_types::FuncBinding::CodeGeneration { + attribute_prototype_id, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + LeafBinding::delete_leaf_func_binding( + &ctx, + attribute_prototype_id.into_raw_id().into(), + ) + .await?; + } + None => { + return Err(FuncAPIError::MissingPrototypeId); + } + } + } else if let frontend_types::FuncBinding::Qualification { + attribute_prototype_id, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + LeafBinding::delete_leaf_func_binding( + &ctx, + attribute_prototype_id.into_raw_id().into(), + ) + .await?; + } + None => { + return Err(FuncAPIError::MissingPrototypeId); + } + } + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + } + _ => { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + }; + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + track( + &posthog_client, + &ctx, + &original_uri, + "delete_binding", + serde_json::json!({ + "how": "/func/delete_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + let types = get_types(&ctx, func_id).await?; + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/binding/update_binding.rs b/lib/sdf-server/src/server/service/v2/func/binding/update_binding.rs new file mode 100644 index 0000000000..4785e8eb19 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/binding/update_binding.rs @@ -0,0 +1,140 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::binding::{action::ActionBinding, leaf::LeafBinding, FuncBindings}, + schema::variant::leaves::LeafInputLocation, + ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, WsEvent, +}; +use si_frontend_types as frontend_types; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, + }, + service::v2::func::{get_types, FuncAPIError, FuncAPIResult}, +}; + +pub async fn update_binding( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + match func.kind { + dal::func::FuncKind::Action => { + for binding in request.bindings { + if let frontend_types::FuncBinding::Action { + action_prototype_id, + kind, + .. + } = binding + { + match (action_prototype_id, kind) { + (Some(action_prototype_id), Some(kind)) => { + ActionBinding::update_action_binding( + &ctx, + action_prototype_id.into_raw_id().into(), + kind.into(), + ) + .await?; + } + _ => { + return Err(FuncAPIError::MissingActionKindForActionFunc); + } + } + } else { + return Err(FuncAPIError::MissingActionKindForActionFunc); + } + } + } + dal::func::FuncKind::CodeGeneration | dal::func::FuncKind::Qualification => { + for binding in request.bindings { + if let frontend_types::FuncBinding::CodeGeneration { + attribute_prototype_id, + inputs, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + let inputs: Vec = + inputs.into_iter().map(|input| input.into()).collect(); + LeafBinding::update_leaf_func_binding( + &ctx, + attribute_prototype_id.into_raw_id().into(), + &inputs, + ) + .await?; + } + None => { + return Err(FuncAPIError::MissingPrototypeId); + } + } + } else if let frontend_types::FuncBinding::Qualification { + attribute_prototype_id, + inputs, + .. + } = binding + { + match attribute_prototype_id { + Some(attribute_prototype_id) => { + let inputs: Vec = + inputs.into_iter().map(|input| input.into()).collect(); + LeafBinding::update_leaf_func_binding( + &ctx, + attribute_prototype_id.into_raw_id().into(), + &inputs, + ) + .await?; + } + None => { + return Err(FuncAPIError::MissingPrototypeId); + } + } + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + } + _ => return Err(FuncAPIError::WrongFunctionKindForBinding), + } + let binding = FuncBindings::from_func_id(&ctx, func_id) + .await? + .into_frontend_type(); + track( + &posthog_client, + &ctx, + &original_uri, + "update_binding", + serde_json::json!({ + "how": "/func/update_binding", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + let types = get_types(&ctx, func_id).await?; + WsEvent::func_bindings_updated(&ctx, binding.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&binding)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/create_func.rs b/lib/sdf-server/src/server/service/v2/func/create_func.rs new file mode 100644 index 0000000000..b5c8c3c767 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/create_func.rs @@ -0,0 +1,233 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use serde::{Deserialize, Serialize}; + +use dal::{ + func::{ + authoring::FuncAuthoringClient, + binding::{ + AttributeArgumentBinding, AttributeFuncArgumentSource, AttributeFuncDestination, + EventualParent, + }, + FuncKind, + }, + schema::variant::leaves::{LeafInputLocation, LeafKind}, + ChangeSet, ChangeSetId, WorkspacePk, WsEvent, +}; +use si_frontend_types::{self as frontend_types, FuncBinding, FuncCode, FuncSummary}; + +use crate::server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, +}; + +use super::{get_code_response, get_types, FuncAPIError, FuncAPIResult}; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CreateFuncRequest { + name: Option, + display_name: Option, + description: Option, + binding: frontend_types::FuncBinding, + kind: FuncKind, +} +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CreateFuncResponse { + summary: FuncSummary, + code: FuncCode, +} + +pub async fn create_func( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + if let Some(name) = request.name.as_deref() { + if dal::func::is_intrinsic(name) + || ["si:resourcePayloadToValue", "si:normalizeToArray"].contains(&name) + { + return Err(FuncAPIError::FuncNameReserved(name.into())); + } + } + + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + let func = match request.kind { + FuncKind::Action => { + if let FuncBinding::Action { + schema_variant_id: Some(schema_variant_id), + kind: Some(kind), + .. + } = request.binding + { + FuncAuthoringClient::create_new_action_func( + &ctx, + request.name, + kind.into(), + schema_variant_id.into(), + ) + .await? + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + FuncKind::Attribute => { + if let FuncBinding::Attribute { + prop_id, + output_socket_id, + argument_bindings, + component_id, + .. + } = request.binding + { + let output_location = if let Some(prop_id) = prop_id { + AttributeFuncDestination::Prop(prop_id.into()) + } else if let Some(output_socket_id) = output_socket_id { + AttributeFuncDestination::OutputSocket(output_socket_id.into()) + } else { + return Err(FuncAPIError::MissingOutputLocationForAttributeFunc); + }; + let eventual_parent = + component_id.map(|component_id| EventualParent::Component(component_id.into())); + let mut arg_bindings = vec![]; + for arg_binding in argument_bindings { + let input_location = if let Some(prop_id) = arg_binding.prop_id { + AttributeFuncArgumentSource::Prop(prop_id.into()) + } else if let Some(input_socket_id) = arg_binding.input_socket_id { + AttributeFuncArgumentSource::InputSocket(input_socket_id.into()) + } else { + return Err(FuncAPIError::MissingInputLocationForAttributeFunc); + }; + arg_bindings.push(AttributeArgumentBinding { + func_argument_id: arg_binding.func_argument_id.into(), + attribute_prototype_argument_id: arg_binding + .attribute_prototype_argument_id + .map(|a| a.into()), + attribute_func_input_location: input_location, + }); + } + + FuncAuthoringClient::create_new_attribute_func( + &ctx, + request.name, + eventual_parent, + output_location, + arg_bindings, + ) + .await? + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + FuncKind::Authentication => { + if let FuncBinding::Authentication { + schema_variant_id, + func_id: _, + } = request.binding + { + FuncAuthoringClient::create_new_auth_func( + &ctx, + request.name, + schema_variant_id.into(), + ) + .await? + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + FuncKind::CodeGeneration => { + if let FuncBinding::CodeGeneration { + schema_variant_id: Some(schema_variant_id), + inputs, + .. + } = request.binding + { + let inputs = if inputs.is_empty() { + vec![LeafInputLocation::Domain] + } else { + inputs.into_iter().map(|input| input.into()).collect() + }; + FuncAuthoringClient::create_new_leaf_func( + &ctx, + request.name, + LeafKind::CodeGeneration, + EventualParent::SchemaVariant(schema_variant_id.into()), + &inputs, + ) + .await? + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + FuncKind::Qualification => { + if let FuncBinding::Qualification { + schema_variant_id: Some(schema_variant_id), + inputs, + .. + } = request.binding + { + let inputs = if inputs.is_empty() { + vec![LeafInputLocation::Domain, LeafInputLocation::Code] + } else { + inputs.into_iter().map(|input| input.into()).collect() + }; + + FuncAuthoringClient::create_new_leaf_func( + &ctx, + request.name, + LeafKind::Qualification, + EventualParent::SchemaVariant(schema_variant_id.into()), + &inputs, + ) + .await? + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + _ => return Err(FuncAPIError::WrongFunctionKindForBinding), + }; + + let types = get_types(&ctx, func.id).await?; + let code = get_code_response(&ctx, func.id).await?; + let summary = func.into_frontend_type(&ctx).await?; + WsEvent::func_created(&ctx, summary.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + track( + &posthog_client, + &ctx, + &original_uri, + "created_func", + serde_json::json!({ + "how": "/func/created_func", + "func_id": summary.func_id, + "func_name": summary.name.to_owned(), + "func_kind": summary.kind, + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + + Ok(response.body(serde_json::to_string(&CreateFuncResponse { + summary, + code, + })?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/execute_func.rs b/lib/sdf-server/src/server/service/v2/func/execute_func.rs new file mode 100644 index 0000000000..64eb48de6d --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/execute_func.rs @@ -0,0 +1,54 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, +}; + +use dal::{ + func::authoring::FuncAuthoringClient, ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, +}; + +use crate::server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, +}; + +use super::FuncAPIResult; + +pub async fn execute_func( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + FuncAuthoringClient::execute_func(&ctx, func_id).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + + // ws event? + + track( + &posthog_client, + &ctx, + &original_uri, + "execute_func", + serde_json::json!({ + "how": "/func/execute_func", + "func_id": func_id, + "func_name": func.name.clone(), + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(axum::body::Empty::new())?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/get_code.rs b/lib/sdf-server/src/server/service/v2/func/get_code.rs new file mode 100644 index 0000000000..38001479b3 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/get_code.rs @@ -0,0 +1,37 @@ +use axum::{ + extract::{OriginalUri, Path}, + Json, +}; +use dal::{ChangeSetId, FuncId, WorkspacePk}; + +use serde::{Deserialize, Serialize}; +use si_frontend_types::FuncCode; + +use crate::server::extract::{AccessBuilder, HandlerContext, PosthogClient}; + +use super::{get_code_response, FuncAPIResult}; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetRequest { + pub func_ids: Vec, +} + +pub async fn get_code( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(_posthog_client): PosthogClient, + OriginalUri(_original_uri): OriginalUri, + Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>, + Json(request): Json, +) -> FuncAPIResult>> { + let ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let mut funcs = Vec::new(); + + for func_id in request.func_ids { + funcs.push(get_code_response(&ctx, func_id).await?); + } + Ok(Json(funcs)) +} diff --git a/lib/sdf-server/src/server/service/v2/func/list_funcs.rs b/lib/sdf-server/src/server/service/v2/func/list_funcs.rs new file mode 100644 index 0000000000..d5fc89b923 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/list_funcs.rs @@ -0,0 +1,36 @@ +use axum::{ + extract::{OriginalUri, Path}, + Json, +}; +use dal::{ChangeSetId, Func, FuncId, WorkspacePk}; + +use serde::{Deserialize, Serialize}; +use si_frontend_types as frontend_types; + +use crate::server::extract::{AccessBuilder, HandlerContext, PosthogClient}; + +use super::FuncAPIResult; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetRequest { + pub func_ids: Vec, +} + +pub async fn list_funcs( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(_posthog_client): PosthogClient, + OriginalUri(_original_uri): OriginalUri, + Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>, +) -> FuncAPIResult>> { + let ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let mut funcs = Vec::new(); + + for func in Func::list(&ctx).await? { + funcs.push(func.into_frontend_type(&ctx).await?); + } + Ok(Json(funcs)) +} diff --git a/lib/sdf-server/src/server/service/v2/func/save_code.rs b/lib/sdf-server/src/server/service/v2/func/save_code.rs new file mode 100644 index 0000000000..c62c0f51ef --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/save_code.rs @@ -0,0 +1,61 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; + +use dal::{ + func::authoring::FuncAuthoringClient, ChangeSet, ChangeSetId, Func, FuncId, WorkspacePk, + WsEvent, +}; + +use crate::server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, +}; + +use super::{get_code_response, FuncAPIResult}; + +pub async fn save_code( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + FuncAuthoringClient::save_code(&ctx, func_id, request).await?; + let func_code = get_code_response(&ctx, func_id).await?; + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + WsEvent::func_code_saved(&ctx, func_code) + .await? + .publish_on_commit(&ctx) + .await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "save_func_code", + serde_json::json!({ + "how": "/func/save_code", + "func_id": func_id, + "func_name": func.name.clone(), + "func_kind": func.kind.clone(), + }), + ); + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(axum::body::Empty::new())?) +} diff --git a/lib/sdf-server/src/server/service/v2/func/test_execute.rs b/lib/sdf-server/src/server/service/v2/func/test_execute.rs new file mode 100644 index 0000000000..3aef09c64f --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/test_execute.rs @@ -0,0 +1,73 @@ +use axum::{ + extract::{OriginalUri, Path}, + Json, +}; +use serde::{Deserialize, Serialize}; + +use dal::{ + func::authoring::FuncAuthoringClient, ChangeSetId, ComponentId, Func, FuncId, WorkspacePk, +}; +use si_events::FuncRunId; + +use crate::server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, +}; + +use super::FuncAPIResult; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TestExecuteFuncRequest { + pub args: serde_json::Value, + pub code: String, + pub component_id: ComponentId, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TestExecuteFuncResponse { + func_run_id: FuncRunId, +} + +pub async fn test_execute( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult> { + let ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + // should we force a changeset to test execute? + // let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + let func = Func::get_by_id_or_error(&ctx, func_id).await?; + let func_run_id = FuncAuthoringClient::test_execute_func( + &ctx, + func_id, + request.args, + Some(request.code), + request.component_id, + ) + .await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "test_execute", + serde_json::json!({ + "how": "/func/test_execute", + "func_id": func_id, + "func_name": func.name.clone(), + "component_id": request.component_id, + }), + ); + + ctx.commit().await?; + + Ok(Json(TestExecuteFuncResponse { func_run_id })) +} diff --git a/lib/sdf-server/src/server/service/v2/func/update_func.rs b/lib/sdf-server/src/server/service/v2/func/update_func.rs new file mode 100644 index 0000000000..0cd4230cdb --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/update_func.rs @@ -0,0 +1,70 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use dal::{ + func::authoring::FuncAuthoringClient, ChangeSet, ChangeSetId, FuncId, WorkspacePk, WsEvent, +}; + +use serde::{Deserialize, Serialize}; + +use crate::server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + tracking::track, +}; + +use super::{get_types, FuncAPIResult}; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UpdateFuncRequest { + pub display_name: Option, + pub description: Option, +} + +pub async fn update_func( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, func_id)): Path<(WorkspacePk, ChangeSetId, FuncId)>, + Json(request): Json, +) -> FuncAPIResult { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + let updated_func = + FuncAuthoringClient::update_func(&ctx, func_id, request.display_name, request.description) + .await? + .into_frontend_type(&ctx) + .await?; + let types = get_types(&ctx, func_id).await?; + + WsEvent::func_updated(&ctx, updated_func.clone(), types) + .await? + .publish_on_commit(&ctx) + .await?; + track( + &posthog_client, + &ctx, + &original_uri, + "update_func", + serde_json::json!({ + "how": "/func/update_binding", + "func_id": func_id, + "func_name": updated_func.name.clone(), + "func_kind": updated_func.kind.clone(), + }), + ); + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + Ok(response.body(serde_json::to_string(&updated_func)?)?) +} diff --git a/lib/sdf-server/src/server/service/v2/variant.rs b/lib/sdf-server/src/server/service/v2/variant.rs new file mode 100644 index 0000000000..a2869fb299 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/variant.rs @@ -0,0 +1,124 @@ +use axum::{ + extract::{OriginalUri, Path}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; +use dal::{ChangeSetId, Schema, SchemaVariant, SchemaVariantId, WorkspacePk}; +use si_frontend_types as frontend_types; +use thiserror::Error; + +use crate::{ + server::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + state::AppState, + tracking::track, + }, + service::ApiError, +}; + +pub fn v2_routes() -> Router { + Router::new() + .route("/", get(list_schema_variants)) + .route("/:schema_variant_id", get(get_variant)) +} + +pub async fn list_schema_variants( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>, +) -> Result>, SchemaVariantsAPIError> { + let ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + + let mut schema_variants = Vec::new(); + + for schema_id in Schema::list_ids(&ctx).await? { + // NOTE(fnichol): Yes there is `SchemaVariant::list_default_ids()`, but shortly we'll be + // asking for more than only the defaults which reduces us back to looping through schemas + // to filter appropriate schema variants. + let schema_variant = SchemaVariant::get_default_for_schema(&ctx, schema_id).await?; + if !schema_variant.ui_hidden() { + schema_variants.push(schema_variant.into_frontend_type(&ctx, schema_id).await?); + } + } + + track( + &posthog_client, + &ctx, + &original_uri, + "list_schema_variants", + serde_json::json!({}), + ); + + Ok(Json(schema_variants)) +} + +pub async fn get_variant( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Path((_workspace_pk, change_set_id, schema_variant_id)): Path<( + WorkspacePk, + ChangeSetId, + SchemaVariantId, + )>, +) -> Result, SchemaVariantsAPIError> { + let ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + + let schema_variant = SchemaVariant::get_by_id(&ctx, schema_variant_id).await?; + let schema_id = SchemaVariant::schema_id_for_schema_variant_id(&ctx, schema_variant_id).await?; + let schema_variant = schema_variant.into_frontend_type(&ctx, schema_id).await?; + + // Ported from `lib/sdf-server/src/server/service/variant/get_variant.rs`, so changes may be + // desired here... + + track( + &posthog_client, + &ctx, + &original_uri, + "get_variant", + serde_json::json!({ + "schema_name": &schema_variant.schema_name, + "variant_category": &schema_variant.category, + "variant_menu_name": schema_variant.display_name, + "variant_id": schema_variant.schema_variant_id, + "schema_id": schema_variant.schema_id, + "variant_component_type": schema_variant.component_type, + }), + ); + + Ok(Json(schema_variant)) +} +#[remain::sorted] +#[derive(Debug, Error)] +pub enum SchemaVariantsAPIError { + #[error("schema error: {0}")] + Schema(#[from] dal::SchemaError), + #[error("schema error: {0}")] + SchemaVariant(#[from] dal::SchemaVariantError), + #[error("transactions error: {0}")] + Transactions(#[from] dal::TransactionsError), +} + +impl IntoResponse for SchemaVariantsAPIError { + fn into_response(self) -> Response { + let status_code = match &self { + Self::Transactions(dal::TransactionsError::BadWorkspaceAndChangeSet) => { + StatusCode::FORBIDDEN + } + // When a graph node cannot be found for a schema variant, it is not found + Self::SchemaVariant(dal::SchemaVariantError::NotFound(_)) => StatusCode::NOT_FOUND, + _ => ApiError::DEFAULT_ERROR_STATUS_CODE, + }; + + ApiError::new(status_code, self).into_response() + } +} diff --git a/lib/si-events-rs/src/func.rs b/lib/si-events-rs/src/func.rs index 750cc15adc..156d545548 100644 --- a/lib/si-events-rs/src/func.rs +++ b/lib/si-events-rs/src/func.rs @@ -1,3 +1,4 @@ use crate::id; id!(FuncId); +id!(FuncArgumentId); diff --git a/lib/si-events-rs/src/func_run.rs b/lib/si-events-rs/src/func_run.rs index f8732b88ac..1e3cb99dcf 100644 --- a/lib/si-events-rs/src/func_run.rs +++ b/lib/si-events-rs/src/func_run.rs @@ -10,6 +10,8 @@ id!(ComponentId); id!(AttributeValueId); id!(ActionId); id!(ActionPrototypeId); +id!(AttributePrototypeId); +id!(AttributePrototypeArgumentId); #[derive(AsRefStr, Deserialize, Display, Serialize, Debug, Eq, PartialEq, Clone, Copy)] pub enum FuncRunState { @@ -106,7 +108,7 @@ pub enum FuncBackendResponseType { } #[remain::sorted] -#[derive(AsRefStr, Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Display)] +#[derive(AsRefStr, Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Display, Hash)] pub enum ActionKind { /// Create the "outside world" version of the modeled object. Create, diff --git a/lib/si-events-rs/src/lib.rs b/lib/si-events-rs/src/lib.rs index 749d1eec3c..537db9b972 100644 --- a/lib/si-events-rs/src/lib.rs +++ b/lib/si-events-rs/src/lib.rs @@ -25,16 +25,17 @@ pub use crate::{ cas::CasValue, content_hash::ContentHash, encrypted_secret::EncryptedSecretKey, - func::FuncId, + func::{FuncArgumentId, FuncId}, func_execution::*, func_run::{ - ActionId, ActionKind, ActionPrototypeId, ActionResultState, AttributeValueId, ComponentId, - FuncBackendKind, FuncBackendResponseType, FuncKind, FuncRun, FuncRunBuilder, - FuncRunBuilderError, FuncRunId, FuncRunState, FuncRunValue, + ActionId, ActionKind, ActionPrototypeId, ActionResultState, AttributePrototypeArgumentId, + AttributePrototypeId, AttributeValueId, ComponentId, FuncBackendKind, + FuncBackendResponseType, FuncKind, FuncRun, FuncRunBuilder, FuncRunBuilderError, FuncRunId, + FuncRunState, FuncRunValue, }, func_run_log::{FuncRunLog, FuncRunLogId, OutputLine}, schema::SchemaId, - schema_variant::SchemaVariantId, + schema_variant::{PropId, SchemaVariantId}, socket::{InputSocketId, OutputSocketId}, tenancy::ChangeSetId, tenancy::Tenancy, diff --git a/lib/si-events-rs/src/schema_variant.rs b/lib/si-events-rs/src/schema_variant.rs index aa31ff3a2d..1f8c3d4197 100644 --- a/lib/si-events-rs/src/schema_variant.rs +++ b/lib/si-events-rs/src/schema_variant.rs @@ -1,3 +1,4 @@ use crate::id; id!(SchemaVariantId); +id!(PropId); diff --git a/lib/si-frontend-types-rs/src/func.rs b/lib/si-frontend-types-rs/src/func.rs new file mode 100644 index 0000000000..ca16e2d53e --- /dev/null +++ b/lib/si-frontend-types-rs/src/func.rs @@ -0,0 +1,204 @@ +use serde::{Deserialize, Serialize}; +use si_events::{ + ActionKind, ActionPrototypeId, AttributePrototypeArgumentId, AttributePrototypeId, ComponentId, + FuncArgumentId, FuncId, FuncKind, InputSocketId, OutputSocketId, PropId, SchemaVariantId, + Timestamp, +}; +use strum::{AsRefStr, Display, EnumIter, EnumString}; + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum IntrinsicFuncKind { + Identity, + SetArray, + SetBoolean, + SetInteger, + SetJson, + SetMap, + SetObject, + SetString, + Unset, + Validation, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncSummary { + pub func_id: FuncId, + pub kind: FuncKind, + pub name: String, + pub display_name: Option, + pub is_locked: bool, + pub arguments: Vec, + #[serde(flatten)] + pub bindings: FuncBindings, +} +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncArgument { + pub id: Option, + pub name: String, + pub kind: FuncArgumentKind, + pub element_kind: Option, + #[serde(flatten)] + pub timestamp: Timestamp, +} + +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncCode { + pub func_id: FuncId, + pub code: String, + pub types: String, +} +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FuncBindings { + pub bindings: Vec, +} +#[remain::sorted] +#[derive( + AsRefStr, + Clone, + Debug, + Deserialize, + EnumString, + Eq, + Serialize, + Display, + EnumIter, + PartialEq, + Hash, +)] +#[serde(rename_all = "camelCase", tag = "bindingKind")] +pub enum FuncBinding { + #[serde(rename_all = "camelCase")] + Action { + // unique ids + schema_variant_id: Option, + action_prototype_id: Option, + func_id: Option, + //thing that can be updated + kind: Option, + }, + #[serde(rename_all = "camelCase")] + Attribute { + // unique ids + func_id: Option, + attribute_prototype_id: Option, + // things needed for create + component_id: Option, + schema_variant_id: Option, + + // things that can be updated + prop_id: Option, + output_socket_id: Option, + + // can optionally send arguments when creating the prototype, + // or update them later individually + argument_bindings: Vec, + }, + #[serde(rename_all = "camelCase")] + Authentication { + // unique ids + schema_variant_id: SchemaVariantId, + func_id: FuncId, + }, + #[serde(rename_all = "camelCase")] + CodeGeneration { + // unique ids + schema_variant_id: Option, + component_id: Option, + func_id: Option, + attribute_prototype_id: Option, + + // thing that can be updated + inputs: Vec, + }, + #[serde(rename_all = "camelCase")] + Qualification { + // unique ids + schema_variant_id: Option, + component_id: Option, + func_id: Option, + attribute_prototype_id: Option, + + // thing that can be updated + inputs: Vec, + }, +} + +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct AttributeArgumentBinding { + pub func_argument_id: FuncArgumentId, + pub attribute_prototype_argument_id: Option, + pub prop_id: Option, + pub input_socket_id: Option, +} + +/// This enum provides available child [`Prop`](crate::Prop) trees of [`RootProp`](crate::RootProp) +/// that can be used as "inputs" for [`Funcs`](crate::Func) on leaves. +/// +/// _Note: not all [`children`](crate::RootPropChild) of [`RootProp`](crate::RootProp) can be used +/// as "inputs" in order to prevent cycles. This enum provides an approved subset of those +/// children_. +#[remain::sorted] +#[derive( + AsRefStr, + Clone, + Copy, + Debug, + Deserialize, + EnumString, + Eq, + Serialize, + Display, + EnumIter, + Ord, + PartialEq, + PartialOrd, + Hash, +)] +#[serde(rename_all = "camelCase")] +pub enum LeafInputLocation { + /// The input location corresponding to "/root/code". + Code, + /// The input location corresponding to "/root/deleted_at" + DeletedAt, + /// The input location corresponding to "/root/domain". + Domain, + /// The input location corresponding to "/root/resource". + Resource, + /// The input location corresponding to "/root/secrets". + Secrets, +} + +#[remain::sorted] +#[derive( + AsRefStr, + Clone, + Copy, + Debug, + Deserialize, + EnumString, + Eq, + Serialize, + Display, + EnumIter, + Ord, + PartialEq, + PartialOrd, + Hash, +)] +#[serde(rename_all = "camelCase")] +pub enum FuncArgumentKind { + Any, + Array, + Boolean, + Integer, + Json, + Map, + Object, + String, +} diff --git a/lib/si-frontend-types-rs/src/lib.rs b/lib/si-frontend-types-rs/src/lib.rs index 487b7bf173..b91bd7c117 100644 --- a/lib/si-frontend-types-rs/src/lib.rs +++ b/lib/si-frontend-types-rs/src/lib.rs @@ -1,3 +1,10 @@ +mod func; mod schema_variant; -pub use crate::schema_variant::{ComponentType, InputSocket, OutputSocket, SchemaVariant}; +pub use crate::func::{ + AttributeArgumentBinding, FuncArgument, FuncArgumentKind, FuncBinding, FuncBindings, FuncCode, + FuncSummary, LeafInputLocation, +}; +pub use crate::schema_variant::{ + ComponentType, InputSocket, OutputSocket, Prop, PropKind, SchemaVariant, +}; diff --git a/lib/si-frontend-types-rs/src/schema_variant.rs b/lib/si-frontend-types-rs/src/schema_variant.rs index 2b6c1a51f7..001f58bdfb 100644 --- a/lib/si-frontend-types-rs/src/schema_variant.rs +++ b/lib/si-frontend-types-rs/src/schema_variant.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; -use si_events::{FuncId, InputSocketId, OutputSocketId, SchemaId, SchemaVariantId, Timestamp}; +use si_events::{ + FuncId, InputSocketId, OutputSocketId, PropId, SchemaId, SchemaVariantId, Timestamp, +}; use strum::{AsRefStr, Display, EnumIter, EnumString}; #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] @@ -19,6 +21,7 @@ pub struct SchemaVariant { pub component_type: ComponentType, pub input_sockets: Vec, pub output_sockets: Vec, + pub props: Vec, pub is_locked: bool, #[serde(flatten)] pub timestamp: Timestamp, @@ -53,6 +56,7 @@ pub enum ComponentType { pub struct InputSocket { pub id: InputSocketId, pub name: String, + pub eligible_to_send_data: bool, } #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] @@ -60,4 +64,27 @@ pub struct InputSocket { pub struct OutputSocket { pub id: OutputSocketId, pub name: String, + pub eligible_to_receive_data: bool, +} +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Prop { + pub id: PropId, + pub kind: PropKind, + pub name: String, + pub path: String, + pub eligible_to_receive_data: bool, + pub eligible_to_send_data: bool, +} +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum PropKind { + Any, + Array, + Boolean, + Integer, + Json, + Map, + Object, + String, }