From b92534c8bee6ded7b6d7831eb93fb4c7ad75cd7a Mon Sep 17 00:00:00 2001 From: Brit Myers Date: Fri, 28 Jun 2024 19:32:22 -0400 Subject: [PATCH 1/2] Create V2 routes (with new path based structure) for all Func Authoring functionality. Refactor how non-attribute functions are authored to have more granular CRUD-like capabilities for functions and function bindings. Create frontend types in separate crate, to decouple the current front end functionality from the underlying interface for function authoring. Fire WSEvents that include the front end types as the payload. --- lib/dal/BUCK | 1 + lib/dal/src/action/prototype.rs | 13 +- lib/dal/src/attribute/prototype.rs | 21 +- lib/dal/src/attribute/prototype/argument.rs | 12 + lib/dal/src/component.rs | 6 + lib/dal/src/func.rs | 101 +++- lib/dal/src/func/argument.rs | 42 ++ lib/dal/src/func/associations.rs | 18 +- lib/dal/src/func/associations/bags.rs | 2 +- lib/dal/src/func/authoring.rs | 163 +++++ lib/dal/src/func/authoring/save.rs | 6 + lib/dal/src/func/binding.rs | 560 ++++++++++++++++++ lib/dal/src/func/binding/action.rs | 148 +++++ lib/dal/src/func/binding/attribute.rs | 500 ++++++++++++++++ .../src/func/binding/attribute_argument.rs | 123 ++++ lib/dal/src/func/binding/authentication.rs | 57 ++ lib/dal/src/func/binding/leaf.rs | 277 +++++++++ lib/dal/src/func/runner.rs | 12 + lib/dal/src/prop.rs | 45 ++ lib/dal/src/schema/variant.rs | 16 +- lib/dal/src/schema/variant/authoring.rs | 95 ++- lib/dal/src/schema/variant/leaves.rs | 25 +- lib/dal/src/socket/input.rs | 1 + lib/dal/src/socket/output.rs | 2 + lib/dal/src/ws_event.rs | 7 +- .../tests/integration_test/func/authoring.rs | 1 + .../func/authoring/binding.rs | 3 + .../func/authoring/binding/action.rs | 328 ++++++++++ .../func/authoring/binding/attribute.rs | 269 +++++++++ .../func/authoring/binding/authentication.rs | 74 +++ .../func/authoring/create_func.rs | 518 ++++++++++++++++ .../func/authoring/save_func.rs | 5 +- lib/sdf-server/src/server/service/v2.rs | 243 ++++---- lib/sdf-server/src/server/service/v2/func.rs | 170 ++++++ .../src/server/service/v2/func/argument.rs | 3 + .../v2/func/argument/create_argument.rs | 73 +++ .../v2/func/argument/delete_argument.rs | 67 +++ .../v2/func/argument/update_argument.rs | 77 +++ .../src/server/service/v2/func/binding.rs | 4 + .../service/v2/func/binding/attribute.rs | 3 + .../attribute/create_attribute_binding.rs | 126 ++++ .../attribute/reset_attribute_binding.rs | 86 +++ .../attribute/update_attribute_binding.rs | 106 ++++ .../service/v2/func/binding/create_binding.rs | 155 +++++ .../service/v2/func/binding/delete_binding.rs | 124 ++++ .../service/v2/func/binding/update_binding.rs | 140 +++++ .../src/server/service/v2/func/create_func.rs | 284 +++++++++ .../server/service/v2/func/execute_func.rs | 54 ++ .../src/server/service/v2/func/get_code.rs | 37 ++ .../src/server/service/v2/func/list_funcs.rs | 36 ++ .../src/server/service/v2/func/save_code.rs | 61 ++ .../server/service/v2/func/test_execute.rs | 73 +++ .../src/server/service/v2/func/update_func.rs | 70 +++ .../src/server/service/v2/variant.rs | 124 ++++ lib/si-events-rs/src/func.rs | 1 + lib/si-events-rs/src/func_run.rs | 4 +- lib/si-events-rs/src/lib.rs | 11 +- lib/si-events-rs/src/schema_variant.rs | 1 + lib/si-frontend-types-rs/src/func.rs | 203 +++++++ lib/si-frontend-types-rs/src/lib.rs | 9 +- .../src/schema_variant.rs | 29 +- 61 files changed, 5621 insertions(+), 204 deletions(-) create mode 100644 lib/dal/src/func/binding.rs create mode 100644 lib/dal/src/func/binding/action.rs create mode 100644 lib/dal/src/func/binding/attribute.rs create mode 100644 lib/dal/src/func/binding/attribute_argument.rs create mode 100644 lib/dal/src/func/binding/authentication.rs create mode 100644 lib/dal/src/func/binding/leaf.rs create mode 100644 lib/dal/tests/integration_test/func/authoring/binding.rs create mode 100644 lib/dal/tests/integration_test/func/authoring/binding/action.rs create mode 100644 lib/dal/tests/integration_test/func/authoring/binding/attribute.rs create mode 100644 lib/dal/tests/integration_test/func/authoring/binding/authentication.rs create mode 100644 lib/sdf-server/src/server/service/v2/func.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/argument.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/argument/create_argument.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/argument/delete_argument.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/argument/update_argument.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/attribute.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/attribute/create_attribute_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/attribute/reset_attribute_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/attribute/update_attribute_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/create_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/delete_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/binding/update_binding.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/create_func.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/execute_func.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/get_code.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/list_funcs.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/save_code.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/test_execute.rs create mode 100644 lib/sdf-server/src/server/service/v2/func/update_func.rs create mode 100644 lib/sdf-server/src/server/service/v2/variant.rs create mode 100644 lib/si-frontend-types-rs/src/func.rs 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 525eb19b73..9e8aa6c032 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -201,6 +201,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/authoring/save.rs b/lib/dal/src/func/authoring/save.rs index 852265cc42..47bcac7944 100644 --- a/lib/dal/src/func/authoring/save.rs +++ b/lib/dal/src/func/authoring/save.rs @@ -18,6 +18,10 @@ use crate::{ SchemaVariantId, WorkspaceSnapshotError, }; +// create_association +// delete_association(?) +// update_association + #[instrument( name = "func.authoring.save_func.update_associations", level = "debug", @@ -362,6 +366,8 @@ pub(crate) async fn reset_attribute_prototype( AttributeValue::use_default_prototype(ctx, attribute_value_id).await?; return Ok(()); } + // Find the last locked schema variant's equivalent and set the prototype? + // regeneration? } // If we aren't trying to use the default prototype, or the default prototype is the same as the diff --git a/lib/dal/src/func/binding.rs b/lib/dal/src/func/binding.rs new file mode 100644 index 0000000000..af2a5acd64 --- /dev/null +++ b/lib/dal/src/func/binding.rs @@ -0,0 +1,560 @@ +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, + func_bindings: si_frontend_types::FuncBindings, + types: String, +} + +impl WsEvent { + pub async fn func_bindings_updated( + ctx: &DalContext, + func_bindings: si_frontend_types::FuncBindings, + types: String, + ) -> WsEventResult { + WsEvent::new( + ctx, + WsPayload::FuncBindingsUpdated(FuncBindingsWsEventPayload { + change_set_id: ctx.change_set_id(), + func_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/func/runner.rs b/lib/dal/src/func/runner.rs index c3e6fc90c0..1708945985 100644 --- a/lib/dal/src/func/runner.rs +++ b/lib/dal/src/func/runner.rs @@ -1049,6 +1049,7 @@ impl FuncRunner { let mut funcs_and_secrets = vec![]; for secret_prop_id in secret_props { + // if manually set: do this let auth_funcs = Self::auth_funcs_for_secret_prop_id( ctx, secret_prop_id, @@ -1079,6 +1080,17 @@ impl FuncRunner { Err(other_err) => return Err(other_err)?, } } + // if not manually set - find input socket + // find connected / inferred matching output socket + // find component of output socket + // find sv of component + // get auth func for it + + // check it's secret props - repeat --- + // if manually set do the above + // if not, check input socket + + // on and on if let Some(value) = maybe_value { let key = Secret::key_from_value_in_attribute_value(value)?; diff --git a/lib/dal/src/prop.rs b/lib/dal/src/prop.rs index 713afbcb7c..07d1eef108 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 869b614704..18b30fdda6 100644 --- a/lib/dal/src/schema/variant.rs +++ b/lib/dal/src/schema/variant.rs @@ -216,6 +216,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(), @@ -240,6 +246,7 @@ impl SchemaVariant { .collect(), is_locked: self.is_locked, timestamp: self.timestamp.into(), + props: front_end_props, }) } } @@ -1285,8 +1292,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, @@ -1824,7 +1831,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. @@ -1854,7 +1861,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..cc3d8fee45 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_recieve_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..8afd7ffdd7 100644 --- a/lib/sdf-server/src/server/service/v2.rs +++ b/lib/sdf-server/src/server/service/v2.rs @@ -1,21 +1,8 @@ -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, -}; - -use super::ApiError; +use axum::{routing::get, Router}; + +use crate::server::state::AppState; +pub mod func; +pub mod variant; pub fn routes() -> Router { const PREFIX: &str = "/workspaces/:workspace_id/change-sets/:change_set_id"; @@ -23,119 +10,119 @@ pub fn routes() -> Router { Router::new() .route( &format!("{PREFIX}/schema-variants"), - get(list_schema_variants), + get(variant::list_schema_variants), ) .route( &format!("{PREFIX}/schema-variants/:schema_variant_id"), - get(get_variant), + get(variant::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(); - - // 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() - } -} +// 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() +// } +// } 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..62ae1b0037 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func.rs @@ -0,0 +1,170 @@ +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), + ) +} + +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?, + }) +} +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..1434bbaa38 --- /dev/null +++ b/lib/sdf-server/src/server/service/v2/func/create_func.rs @@ -0,0 +1,284 @@ +use axum::{ + extract::{OriginalUri, Path}, + response::IntoResponse, + Json, +}; +use serde::{Deserialize, Serialize}; + +use dal::{ + func::{ + authoring::FuncAuthoringClient, + binding::{ + AttributeArgumentBinding, AttributeFuncArgumentSource, AttributeFuncDestination, + EventualParent, FuncBindings, + }, + FuncKind, + }, + schema::variant::leaves::{LeafInputLocation, LeafKind}, + ChangeSet, ChangeSetId, Func, 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, + binding: frontend_types::FuncBindings, +} + +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 created_func_response = match request.kind { + FuncKind::Action => { + if let FuncBinding::Action { + schema_variant_id: Some(schema_variant_id), + kind: Some(kind), + .. + } = request.binding + { + let created_func = FuncAuthoringClient::create_new_action_func( + &ctx, + request.name, + kind.into(), + schema_variant_id.into(), + ) + .await?; + + let func = Func::get_by_id_or_error(&ctx, created_func.id).await?; + let binding = FuncBindings::from_func_id(&ctx, created_func.id) + .await? + .into_frontend_type(); + CreateFuncResponse { + summary: func.into_frontend_type(&ctx).await?, + code: get_code_response(&ctx, created_func.id).await?, + binding, + } + } 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, + }); + } + + let created_func = FuncAuthoringClient::create_new_attribute_func( + &ctx, + request.name, + eventual_parent, + output_location, + arg_bindings, + ) + .await?; + + let binding = FuncBindings::from_func_id(&ctx, created_func.id) + .await? + .into_frontend_type(); + let func = Func::get_by_id_or_error(&ctx, created_func.id).await?; + CreateFuncResponse { + summary: func.into_frontend_type(&ctx).await?, + code: get_code_response(&ctx, func.id).await?, + binding, + } + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + FuncKind::Authentication => { + if let FuncBinding::Authentication { + schema_variant_id, + func_id: _, + } = request.binding + { + let created_func = FuncAuthoringClient::create_new_auth_func( + &ctx, + request.name, + schema_variant_id.into(), + ) + .await?; + + let binding = FuncBindings::from_func_id(&ctx, created_func.id) + .await? + .into_frontend_type(); + let new_func = Func::get_by_id_or_error(&ctx, created_func.id).await?; + CreateFuncResponse { + summary: new_func.into_frontend_type(&ctx).await?, + code: get_code_response(&ctx, created_func.id).await?, + binding, + } + } 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() + }; + let created_func = FuncAuthoringClient::create_new_leaf_func( + &ctx, + request.name, + LeafKind::CodeGeneration, + EventualParent::SchemaVariant(schema_variant_id.into()), + &inputs, + ) + .await?; + let binding = FuncBindings::from_func_id(&ctx, created_func.id) + .await? + .into_frontend_type(); + let new_func = Func::get_by_id_or_error(&ctx, created_func.id).await?; + CreateFuncResponse { + summary: new_func.into_frontend_type(&ctx).await?, + code: get_code_response(&ctx, created_func.id).await?, + binding, + } + } 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() + }; + + let created_func = FuncAuthoringClient::create_new_leaf_func( + &ctx, + request.name, + LeafKind::Qualification, + EventualParent::SchemaVariant(schema_variant_id.into()), + &inputs, + ) + .await?; + let binding = FuncBindings::from_func_id(&ctx, created_func.id) + .await? + .into_frontend_type(); + let new_func = Func::get_by_id_or_error(&ctx, created_func.id) + .await? + .into_frontend_type(&ctx) + .await?; + CreateFuncResponse { + summary: new_func, + code: get_code_response(&ctx, created_func.id).await?, + binding, + } + } else { + return Err(FuncAPIError::WrongFunctionKindForBinding); + } + } + _ => return Err(FuncAPIError::WrongFunctionKindForBinding), + }; + let types = get_types( + &ctx, + created_func_response.summary.func_id.into_raw_id().into(), + ) + .await?; + + WsEvent::func_created(&ctx, created_func_response.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": created_func_response.summary.func_id, + "func_name": created_func_response.summary.name.to_owned(), + "func_kind": created_func_response.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(&created_func_response)?)?) +} 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..e96ba63420 --- /dev/null +++ b/lib/si-frontend-types-rs/src/func.rs @@ -0,0 +1,203 @@ +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, + 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..1ec60f30c3 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_recieve_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, } From bf23144ff7a498aee2ca0bf1b8bc7a7454646fb4 Mon Sep 17 00:00:00 2001 From: Brit Myers Date: Mon, 1 Jul 2024 11:04:31 -0400 Subject: [PATCH 2/2] configure routes correctly, flatten FuncBindings struct, feedback from Victor! --- lib/dal/src/func/authoring/save.rs | 6 - lib/dal/src/func/binding.rs | 7 +- lib/dal/src/func/runner.rs | 12 -- lib/dal/src/socket/output.rs | 2 +- lib/sdf-server/src/server/service/v2.rs | 130 ++---------------- lib/sdf-server/src/server/service/v2/func.rs | 3 + .../src/server/service/v2/func/create_func.rs | 99 ++++--------- lib/si-frontend-types-rs/src/func.rs | 1 + .../src/schema_variant.rs | 2 +- 9 files changed, 45 insertions(+), 217 deletions(-) diff --git a/lib/dal/src/func/authoring/save.rs b/lib/dal/src/func/authoring/save.rs index 47bcac7944..852265cc42 100644 --- a/lib/dal/src/func/authoring/save.rs +++ b/lib/dal/src/func/authoring/save.rs @@ -18,10 +18,6 @@ use crate::{ SchemaVariantId, WorkspaceSnapshotError, }; -// create_association -// delete_association(?) -// update_association - #[instrument( name = "func.authoring.save_func.update_associations", level = "debug", @@ -366,8 +362,6 @@ pub(crate) async fn reset_attribute_prototype( AttributeValue::use_default_prototype(ctx, attribute_value_id).await?; return Ok(()); } - // Find the last locked schema variant's equivalent and set the prototype? - // regeneration? } // If we aren't trying to use the default prototype, or the default prototype is the same as the diff --git a/lib/dal/src/func/binding.rs b/lib/dal/src/func/binding.rs index af2a5acd64..6fe884434d 100644 --- a/lib/dal/src/func/binding.rs +++ b/lib/dal/src/func/binding.rs @@ -537,21 +537,22 @@ impl FuncBindings { #[serde(rename_all = "camelCase")] pub struct FuncBindingsWsEventPayload { change_set_id: ChangeSetId, - func_bindings: si_frontend_types::FuncBindings, + #[serde(flatten)] + bindings: si_frontend_types::FuncBindings, types: String, } impl WsEvent { pub async fn func_bindings_updated( ctx: &DalContext, - func_bindings: si_frontend_types::FuncBindings, + bindings: si_frontend_types::FuncBindings, types: String, ) -> WsEventResult { WsEvent::new( ctx, WsPayload::FuncBindingsUpdated(FuncBindingsWsEventPayload { change_set_id: ctx.change_set_id(), - func_bindings, + bindings, types, }), ) diff --git a/lib/dal/src/func/runner.rs b/lib/dal/src/func/runner.rs index 1708945985..c3e6fc90c0 100644 --- a/lib/dal/src/func/runner.rs +++ b/lib/dal/src/func/runner.rs @@ -1049,7 +1049,6 @@ impl FuncRunner { let mut funcs_and_secrets = vec![]; for secret_prop_id in secret_props { - // if manually set: do this let auth_funcs = Self::auth_funcs_for_secret_prop_id( ctx, secret_prop_id, @@ -1080,17 +1079,6 @@ impl FuncRunner { Err(other_err) => return Err(other_err)?, } } - // if not manually set - find input socket - // find connected / inferred matching output socket - // find component of output socket - // find sv of component - // get auth func for it - - // check it's secret props - repeat --- - // if manually set do the above - // if not, check input socket - - // on and on if let Some(value) = maybe_value { let key = Secret::key_from_value_in_attribute_value(value)?; diff --git a/lib/dal/src/socket/output.rs b/lib/dal/src/socket/output.rs index cc3d8fee45..fda9fc9118 100644 --- a/lib/dal/src/socket/output.rs +++ b/lib/dal/src/socket/output.rs @@ -496,7 +496,7 @@ impl From for frontend_types::OutputSocket { id: value.id.into(), name: value.name, //default to false, but figure out how to do this better - eligible_to_recieve_data: false, + eligible_to_receive_data: false, } } } diff --git a/lib/sdf-server/src/server/service/v2.rs b/lib/sdf-server/src/server/service/v2.rs index 8afd7ffdd7..75ed837f8c 100644 --- a/lib/sdf-server/src/server/service/v2.rs +++ b/lib/sdf-server/src/server/service/v2.rs @@ -1,128 +1,20 @@ -use axum::{routing::get, Router}; - use crate::server::state::AppState; +use axum::Router; + 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(variant::list_schema_variants), - ) - .route( - &format!("{PREFIX}/schema-variants/:schema_variant_id"), - get(variant::get_variant), + crate::server::service::v2::variant::v2_routes(), ) + .nest( + &format!("{PREFIX}/funcs"), + crate::server::service::v2::func::v2_routes(), + ); + router } - -// 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() -// } -// } diff --git a/lib/sdf-server/src/server/service/v2/func.rs b/lib/sdf-server/src/server/service/v2/func.rs index 62ae1b0037..15ea312c31 100644 --- a/lib/sdf-server/src/server/service/v2/func.rs +++ b/lib/sdf-server/src/server/service/v2/func.rs @@ -147,6 +147,7 @@ pub fn v2_routes() -> Router { ) } +// 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()); @@ -156,6 +157,8 @@ pub async fn get_code_response(ctx: &DalContext, func_id: FuncId) -> FuncAPIResu 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 = [ 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 index 1434bbaa38..b5c8c3c767 100644 --- a/lib/sdf-server/src/server/service/v2/func/create_func.rs +++ b/lib/sdf-server/src/server/service/v2/func/create_func.rs @@ -10,12 +10,12 @@ use dal::{ authoring::FuncAuthoringClient, binding::{ AttributeArgumentBinding, AttributeFuncArgumentSource, AttributeFuncDestination, - EventualParent, FuncBindings, + EventualParent, }, FuncKind, }, schema::variant::leaves::{LeafInputLocation, LeafKind}, - ChangeSet, ChangeSetId, Func, WorkspacePk, WsEvent, + ChangeSet, ChangeSetId, WorkspacePk, WsEvent, }; use si_frontend_types::{self as frontend_types, FuncBinding, FuncCode, FuncSummary}; @@ -40,7 +40,6 @@ pub struct CreateFuncRequest { pub struct CreateFuncResponse { summary: FuncSummary, code: FuncCode, - binding: frontend_types::FuncBindings, } pub async fn create_func( @@ -64,7 +63,7 @@ pub async fn create_func( let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; - let created_func_response = match request.kind { + let func = match request.kind { FuncKind::Action => { if let FuncBinding::Action { schema_variant_id: Some(schema_variant_id), @@ -72,23 +71,13 @@ pub async fn create_func( .. } = request.binding { - let created_func = FuncAuthoringClient::create_new_action_func( + FuncAuthoringClient::create_new_action_func( &ctx, request.name, kind.into(), schema_variant_id.into(), ) - .await?; - - let func = Func::get_by_id_or_error(&ctx, created_func.id).await?; - let binding = FuncBindings::from_func_id(&ctx, created_func.id) - .await? - .into_frontend_type(); - CreateFuncResponse { - summary: func.into_frontend_type(&ctx).await?, - code: get_code_response(&ctx, created_func.id).await?, - binding, - } + .await? } else { return Err(FuncAPIError::WrongFunctionKindForBinding); } @@ -129,24 +118,14 @@ pub async fn create_func( }); } - let created_func = FuncAuthoringClient::create_new_attribute_func( + FuncAuthoringClient::create_new_attribute_func( &ctx, request.name, eventual_parent, output_location, arg_bindings, ) - .await?; - - let binding = FuncBindings::from_func_id(&ctx, created_func.id) - .await? - .into_frontend_type(); - let func = Func::get_by_id_or_error(&ctx, created_func.id).await?; - CreateFuncResponse { - summary: func.into_frontend_type(&ctx).await?, - code: get_code_response(&ctx, func.id).await?, - binding, - } + .await? } else { return Err(FuncAPIError::WrongFunctionKindForBinding); } @@ -157,22 +136,12 @@ pub async fn create_func( func_id: _, } = request.binding { - let created_func = FuncAuthoringClient::create_new_auth_func( + FuncAuthoringClient::create_new_auth_func( &ctx, request.name, schema_variant_id.into(), ) - .await?; - - let binding = FuncBindings::from_func_id(&ctx, created_func.id) - .await? - .into_frontend_type(); - let new_func = Func::get_by_id_or_error(&ctx, created_func.id).await?; - CreateFuncResponse { - summary: new_func.into_frontend_type(&ctx).await?, - code: get_code_response(&ctx, created_func.id).await?, - binding, - } + .await? } else { return Err(FuncAPIError::WrongFunctionKindForBinding); } @@ -189,23 +158,14 @@ pub async fn create_func( } else { inputs.into_iter().map(|input| input.into()).collect() }; - let created_func = FuncAuthoringClient::create_new_leaf_func( + FuncAuthoringClient::create_new_leaf_func( &ctx, request.name, LeafKind::CodeGeneration, EventualParent::SchemaVariant(schema_variant_id.into()), &inputs, ) - .await?; - let binding = FuncBindings::from_func_id(&ctx, created_func.id) - .await? - .into_frontend_type(); - let new_func = Func::get_by_id_or_error(&ctx, created_func.id).await?; - CreateFuncResponse { - summary: new_func.into_frontend_type(&ctx).await?, - code: get_code_response(&ctx, created_func.id).await?, - binding, - } + .await? } else { return Err(FuncAPIError::WrongFunctionKindForBinding); } @@ -223,39 +183,25 @@ pub async fn create_func( inputs.into_iter().map(|input| input.into()).collect() }; - let created_func = FuncAuthoringClient::create_new_leaf_func( + FuncAuthoringClient::create_new_leaf_func( &ctx, request.name, LeafKind::Qualification, EventualParent::SchemaVariant(schema_variant_id.into()), &inputs, ) - .await?; - let binding = FuncBindings::from_func_id(&ctx, created_func.id) - .await? - .into_frontend_type(); - let new_func = Func::get_by_id_or_error(&ctx, created_func.id) - .await? - .into_frontend_type(&ctx) - .await?; - CreateFuncResponse { - summary: new_func, - code: get_code_response(&ctx, created_func.id).await?, - binding, - } + .await? } else { return Err(FuncAPIError::WrongFunctionKindForBinding); } } _ => return Err(FuncAPIError::WrongFunctionKindForBinding), }; - let types = get_types( - &ctx, - created_func_response.summary.func_id.into_raw_id().into(), - ) - .await?; - WsEvent::func_created(&ctx, created_func_response.summary.clone(), types) + 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?; @@ -266,9 +212,9 @@ pub async fn create_func( "created_func", serde_json::json!({ "how": "/func/created_func", - "func_id": created_func_response.summary.func_id, - "func_name": created_func_response.summary.name.to_owned(), - "func_kind": created_func_response.summary.kind, + "func_id": summary.func_id, + "func_name": summary.name.to_owned(), + "func_kind": summary.kind, }), ); @@ -280,5 +226,8 @@ pub async fn create_func( response = response.header("force_change_set_id", force_change_set_id.to_string()); } - Ok(response.body(serde_json::to_string(&created_func_response)?)?) + Ok(response.body(serde_json::to_string(&CreateFuncResponse { + summary, + code, + })?)?) } diff --git a/lib/si-frontend-types-rs/src/func.rs b/lib/si-frontend-types-rs/src/func.rs index e96ba63420..ca16e2d53e 100644 --- a/lib/si-frontend-types-rs/src/func.rs +++ b/lib/si-frontend-types-rs/src/func.rs @@ -30,6 +30,7 @@ pub struct FuncSummary { pub display_name: Option, pub is_locked: bool, pub arguments: Vec, + #[serde(flatten)] pub bindings: FuncBindings, } #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] diff --git a/lib/si-frontend-types-rs/src/schema_variant.rs b/lib/si-frontend-types-rs/src/schema_variant.rs index 1ec60f30c3..001f58bdfb 100644 --- a/lib/si-frontend-types-rs/src/schema_variant.rs +++ b/lib/si-frontend-types-rs/src/schema_variant.rs @@ -64,7 +64,7 @@ pub struct InputSocket { pub struct OutputSocket { pub id: OutputSocketId, pub name: String, - pub eligible_to_recieve_data: bool, + pub eligible_to_receive_data: bool, } #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)] #[serde(rename_all = "camelCase")]