diff --git a/bin/lang-js/src/component.ts b/bin/lang-js/src/component.ts index eb80289334..88eefcab47 100644 --- a/bin/lang-js/src/component.ts +++ b/bin/lang-js/src/component.ts @@ -3,12 +3,14 @@ export interface Component { properties: Record; } +export interface Geometry { + x: number; + y: number; + width?: number; + height?: number; +} + export interface ComponentWithGeometry { properties: Record; - geometry: { - x: string, - y: string, - width?: string, - height?: string, - } + geometry: Geometry; } diff --git a/bin/lang-js/src/function_kinds/management.ts b/bin/lang-js/src/function_kinds/management.ts index a339ce8715..20952e244c 100644 --- a/bin/lang-js/src/function_kinds/management.ts +++ b/bin/lang-js/src/function_kinds/management.ts @@ -7,13 +7,16 @@ import { ResultFailure, ResultSuccess, } from "../function"; -import { ComponentWithGeometry } from "../component"; +import { ComponentWithGeometry, Geometry } from "../component"; import { RequestCtx } from "../request"; const debug = Debug("langJs:management"); export interface ManagementFunc extends Func { - thisComponent: ComponentWithGeometry + thisComponent: ComponentWithGeometry; + components: { + [key: string]: ComponentWithGeometry; + } } export type ManagementFuncResult = @@ -21,15 +24,21 @@ export type ManagementFuncResult = | ManagementFuncResultFailure; export interface ManagementOperations { + create?: { [key: string]: { + kind: string; + properties?: object; + geometry?: Geometry; + } }; update?: { [key: string]: { properties?: object; - } } + geometry?: Geometry; + } }; actions?: { [key: string]: { add?: string[], remove?: string[], } - } + }; } export interface ManagementFuncResultSuccess extends ResultSuccess { @@ -42,14 +51,14 @@ export interface ManagementFuncResultFailure extends ResultFailure { } async function execute( vm: NodeVM, { executionId }: RequestCtx, - { thisComponent }: ManagementFunc, + { thisComponent, components }: ManagementFunc, code: string, ): Promise { let managementResult: Record | undefined | null; try { const runner = vm.run(code); managementResult = await new Promise((resolve) => { - runner({ thisComponent }, (resolution: Record) => resolve(resolution)); + runner({ thisComponent, components }, (resolution: Record) => resolve(resolution)); }); } catch (err) { return failureExecution(err as Error, executionId); diff --git a/lib/cyclone-client/src/client.rs b/lib/cyclone-client/src/client.rs index 78f19399ef..f675a60584 100644 --- a/lib/cyclone-client/src/client.rs +++ b/lib/cyclone-client/src/client.rs @@ -443,7 +443,7 @@ impl Default for ClientConfig { #[allow(clippy::panic, clippy::assertions_on_constants)] #[cfg(test)] mod tests { - use std::{env, path::Path}; + use std::{collections::HashMap, env, path::Path}; use base64::{engine::general_purpose, Engine}; use buck2_resources::Buck2Resources; @@ -1326,9 +1326,11 @@ mod tests { execution_id: "1234".to_string(), handler: "manage".to_string(), this_component: ComponentViewWithGeometry { + kind: None, properties: serde_json::json!({"it": "is", "a": "principle", "of": "music", "to": "repeat the theme"}), geometry: serde_json::json!({"x": "1", "y": "2"}), }, + components: HashMap::new(), code_base64: base64_encode( r#"function manage(input) { console.log('first'); @@ -1408,9 +1410,11 @@ mod tests { execution_id: "1234".to_string(), handler: "manage".to_string(), this_component: ComponentViewWithGeometry { + kind: None, properties: serde_json::json!({"it": "is", "a": "principle", "of": "music", "to": "repeat the theme"}), geometry: serde_json::json!({"x": "1", "y": "2"}), }, + components: HashMap::new(), code_base64: base64_encode( r#"function manage({ thisComponent }) { console.log('first'); diff --git a/lib/cyclone-core/src/component_view.rs b/lib/cyclone-core/src/component_view.rs index 2043dc3e07..2d0ba08718 100644 --- a/lib/cyclone-core/src/component_view.rs +++ b/lib/cyclone-core/src/component_view.rs @@ -34,6 +34,8 @@ impl Default for ComponentView { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ComponentViewWithGeometry { + // This is not component kind. Instead it's a schema name + pub kind: Option, pub properties: Value, pub geometry: Value, } @@ -41,6 +43,7 @@ pub struct ComponentViewWithGeometry { impl Default for ComponentViewWithGeometry { fn default() -> Self { Self { + kind: None, properties: serde_json::json!({}), geometry: serde_json::json!({}), } diff --git a/lib/cyclone-core/src/management.rs b/lib/cyclone-core/src/management.rs index 5c2dde3071..358875ba68 100644 --- a/lib/cyclone-core/src/management.rs +++ b/lib/cyclone-core/src/management.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use telemetry::prelude::*; use telemetry_utils::metric; @@ -11,6 +13,7 @@ pub struct ManagementRequest { pub handler: String, pub code_base64: String, pub this_component: ComponentViewWithGeometry, + pub components: HashMap, pub before: Vec, } diff --git a/lib/dal-test/src/expected.rs b/lib/dal-test/src/expected.rs index 69df0090ce..a50d3f7501 100644 --- a/lib/dal-test/src/expected.rs +++ b/lib/dal-test/src/expected.rs @@ -386,7 +386,7 @@ impl ExpectComponent { .geometry(ctx) .await .expect("get geometry for component") - .raw() + .into_raw() } pub async fn view(self, ctx: &DalContext) -> Option { diff --git a/lib/dal-test/src/test_exclusive_schemas/legos/small.rs b/lib/dal-test/src/test_exclusive_schemas/legos/small.rs index 6fcf1d60a2..549bc7b1ee 100644 --- a/lib/dal-test/src/test_exclusive_schemas/legos/small.rs +++ b/lib/dal-test/src/test_exclusive_schemas/legos/small.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use dal::action::prototype::ActionKind; use dal::pkg::{import_pkg_from_pkg, ImportOptions}; use dal::{BuiltinsResult, DalContext}; @@ -11,6 +13,7 @@ use crate::test_exclusive_schemas::legos::bricks::LegoBricks; use crate::test_exclusive_schemas::{ build_action_func, build_asset_func, build_management_func, build_resource_payload_to_value_func, create_identity_func, PKG_CREATED_BY, PKG_VERSION, + SCHEMA_ID_SMALL_EVEN_LEGO, }; pub(crate) async fn migrate_test_exclusive_schema_small_odd_lego( @@ -59,7 +62,7 @@ pub(crate) async fn migrate_test_exclusive_schema_small_odd_lego( let import_management_func_code = "async function main({ thisComponent }: Input): Promise { const thisProperties = thisComponent.properties; - return { + return { status: 'ok', ops: { update: { @@ -81,6 +84,71 @@ pub(crate) async fn migrate_test_exclusive_schema_small_odd_lego( let import_management_func = build_management_func(import_management_func_code, import_management_func_name)?; + let simple_create_mgmt_func_code = r#" + async function main({ thisComponent, components }: Input): Promise { + const thisName = thisComponent.properties?.si?.name ?? "unknown"; + let create = { + [`${thisName}_clone`]: { + properties: { + ...thisComponent.properties, + }, + geometry: { + x: 10, + y: 20, + } + } + }; + + for (let [id, component] of Object.entries(components)) { + const name = component.properties?.si?.name ?? "unknown"; + let clone_name = `${name}_clone`; + if (clone_name in create) { + clone_name = `${clone_name}-${id}`; + } + create[clone_name] = { + ...component, + }; + } + + return { + status: "ok", + ops: { create }; + } + } + "#; + + let clone_me_mgmt_func_name = "test:cloneMeSmallLego"; + let clone_me_mgmt_func = + build_management_func(simple_create_mgmt_func_code, clone_me_mgmt_func_name)?; + + let update_managed_func_code = r#" + async function main({ thisComponent, components }: Input): Promise { + const thisName = thisComponent.properties?.si?.name ?? "unknown"; + + const update: { [key: string]: unknown } = {}; + + for (let [id, component] of Object.entries(components)) { + let name = component.properties?.si?.name ?? "unknown"; + update[id] = { + properties: { + ...component.properties, + si: { + ...component.properties?.si, + name: `${name} managed by ${thisName}`, + } + }, + }; + } + + return { + status: "ok", + ops: { update }; + } + } + "#; + let update_mgmt_func_name = "test:updateManagedComponent"; + let update_mgmt_func = build_management_func(update_managed_func_code, update_mgmt_func_name)?; + let fn_name = "test:deleteActionSmallLego"; let delete_action_func = build_action_func(delete_action_code, fn_name)?; @@ -149,6 +217,24 @@ pub(crate) async fn migrate_test_exclusive_schema_small_odd_lego( .func_unique_id(&import_management_func.unique_id) .build()?, ) + .management_func( + ManagementFuncSpec::builder() + .name("Clone") + .managed_schemas(Some(HashSet::from([ + SCHEMA_ID_SMALL_EVEN_LEGO.to_string() + ]))) + .func_unique_id(&clone_me_mgmt_func.unique_id) + .build()?, + ) + .management_func( + ManagementFuncSpec::builder() + .name("Update") + .managed_schemas(Some(HashSet::from([ + SCHEMA_ID_SMALL_EVEN_LEGO.to_string() + ]))) + .func_unique_id(&update_mgmt_func.unique_id) + .build()?, + ) .build()?, ) .build()?; @@ -162,6 +248,8 @@ pub(crate) async fn migrate_test_exclusive_schema_small_odd_lego( .func(small_lego_authoring_schema_func) .func(resource_payload_to_value_func) .func(import_management_func) + .func(clone_me_mgmt_func) + .func(update_mgmt_func) .schema(small_lego_schema) .build()?; diff --git a/lib/dal/src/component.rs b/lib/dal/src/component.rs index 4a77402ae5..2f6c71a09a 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -48,7 +48,7 @@ use crate::workspace_snapshot::node_weight::attribute_prototype_argument_node_we use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKind; use crate::workspace_snapshot::node_weight::{ComponentNodeWeight, NodeWeight, NodeWeightError}; use crate::workspace_snapshot::{DependentValueRoot, WorkspaceSnapshotError}; -use crate::{AttributePrototypeId, EdgeWeight, SocketArity}; +use crate::{AttributePrototypeId, EdgeWeight, SchemaId, SocketArity}; use frame::{Frame, FrameError}; use resource::ResourceData; use si_frontend_types::{ @@ -114,6 +114,8 @@ pub enum ComponentError { ComponentMissingTypeValueMaterializedView(ComponentId), #[error("component {0} has no attribute value for the {1} prop")] ComponentMissingValue(ComponentId, PropId), + #[error("component {0} is based on a schema {1} that is not managed by {2}")] + ComponentNotManagedSchema(ComponentId, SchemaId, ComponentId), #[error("connection destination component {0} has no attribute value for input socket {1}")] DestinationComponentMissingAttributeValueForInputSocket(ComponentId, InputSocketId), #[error("diagram error: {0}")] @@ -393,6 +395,13 @@ impl Component { discriminant: EdgeWeightKindDiscriminants::Use, result: ComponentResult, ); + implement_add_edge_to!( + source_id: ComponentId, + destination_id: ComponentId, + add_fn: add_manages_edge_to_component, + discriminant: EdgeWeightKindDiscriminants::Manages, + result: ComponentResult, + ); #[instrument( name = "component.new", @@ -1482,18 +1491,25 @@ impl Component { width: Option>, height: Option>, ) -> ComponentResult<()> { - let mut geometry_pre = self.geometry(ctx).await?; - - let geometry_post = RawGeometry { + let new_geometry = RawGeometry { x: x.into(), y: y.into(), width: width.map(|w| w.into()), height: height.map(|h| h.into()), }; - if geometry_pre.clone().raw() != geometry_post { + self.set_raw_geometry(ctx, new_geometry).await + } + + pub async fn set_raw_geometry( + &mut self, + ctx: &DalContext, + raw_geometry: RawGeometry, + ) -> ComponentResult<()> { + let mut geometry_pre = self.geometry(ctx).await?; + if geometry_pre.clone().into_raw() != raw_geometry { geometry_pre - .update(ctx, geometry_post) + .update(ctx, raw_geometry) .await .map_err(|e| ComponentError::Diagram(e.to_string()))?; } @@ -3353,6 +3369,84 @@ impl Component { }) } + /// Add a [`Manages`](`crate::edge_weight::EdgeWeightKind::Manages`) edge + /// from a manager component to a managed component, if the managed + /// component is based on a managed schema + pub async fn manage_component( + ctx: &DalContext, + manager_component_id: ComponentId, + managed_component_id: ComponentId, + ) -> ComponentResult<()> { + let manager_schema_id = Component::schema_for_component_id(ctx, manager_component_id) + .await? + .id(); + let manager_variant = + Self::schema_variant_for_component_id(ctx, manager_component_id).await?; + let managed_component_schema_id = Self::schema_for_component_id(ctx, managed_component_id) + .await? + .id(); + + let managed_schemas = SchemaVariant::all_managed_schemas(ctx, manager_variant.id()).await?; + + if !managed_schemas.contains(&managed_component_schema_id) + && manager_schema_id != managed_component_schema_id + { + return Err(ComponentError::ComponentNotManagedSchema( + managed_component_id, + managed_component_schema_id, + manager_component_id, + )); + } + + Component::add_manages_edge_to_component( + ctx, + manager_component_id, + managed_component_id, + EdgeWeightKind::Manages, + ) + .await?; + + Ok(()) + } + + /// Return the ids of all the components that manage this component + pub async fn get_managers(&self, ctx: &DalContext) -> ComponentResult> { + let mut result = vec![]; + + let snapshot = ctx.workspace_snapshot()?; + + for source_idx in snapshot + .incoming_sources_for_edge_weight_kind(self.id, EdgeWeightKindDiscriminants::Manages) + .await? + { + let node_weight = snapshot.get_node_weight(source_idx).await?; + if let NodeWeight::Component(_) = &node_weight { + result.push(node_weight.id().into()); + } + } + + Ok(result) + } + + /// Return the ids of all the components managed by this component + pub async fn get_managed(&self, ctx: &DalContext) -> ComponentResult> { + let mut result = vec![]; + + let snapshot = ctx.workspace_snapshot()?; + + for target_idx in snapshot + .outgoing_targets_for_edge_weight_kind(self.id, EdgeWeightKindDiscriminants::Manages) + .await? + { + let node_weight = snapshot.get_node_weight(target_idx).await?; + if let NodeWeight::Component(_) = &node_weight { + result.push(node_weight.id().into()); + } + } + + Ok(result) + } + pub async fn into_frontend_type( &self, ctx: &DalContext, diff --git a/lib/dal/src/diagram/geometry.rs b/lib/dal/src/diagram/geometry.rs index aeabb12700..07f0570f89 100644 --- a/lib/dal/src/diagram/geometry.rs +++ b/lib/dal/src/diagram/geometry.rs @@ -61,7 +61,7 @@ impl Geometry { result: DiagramResult, ); - pub fn raw(self) -> RawGeometry { + pub fn into_raw(self) -> RawGeometry { self.into() } diff --git a/lib/dal/src/func/authoring/ts_types.rs b/lib/dal/src/func/authoring/ts_types.rs index 0773c3600f..b1b953d7fb 100644 --- a/lib/dal/src/func/authoring/ts_types.rs +++ b/lib/dal/src/func/authoring/ts_types.rs @@ -61,23 +61,9 @@ pub(crate) fn compile_return_types( FuncBackendResponseType::Object => "type Output = any;", FuncBackendResponseType::Unset => "type Output = undefined | null;", FuncBackendResponseType::Void => "type Output = void;", - FuncBackendResponseType::Management => { - r#" -type Output = { - status: 'ok' | 'error'; - ops?: { - update?: { [key: string]: { properties?: { [key: string]: unknown } } }; - actions?: { [key: string]: { - add?: ("create" | "update" | "refresh" | "delete" | string)[]; - remove?: ("create" | "update" | "refresh" | "delete" | string)[]; - } } - - }; - message?: string | null; -}; -type Input = { thisComponent: { properties: { [key: string]: unknown } } }; - "# - } + // All of the types for a management function are determined at "run" + // time when compiling binding types + FuncBackendResponseType::Management => "", _ => "", // we no longer serve this from the backend, its static on the front end //FuncBackendResponseType::SchemaVariantDefinition => SCHEMA_VARIANT_DEFINITION_TYPES diff --git a/lib/dal/src/func/backend/management.rs b/lib/dal/src/func/backend/management.rs index 2caabb9396..f9ccb6b17b 100644 --- a/lib/dal/src/func/backend/management.rs +++ b/lib/dal/src/func/backend/management.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use async_trait::async_trait; use serde::{Deserialize, Serialize}; use veritech_client::{ @@ -12,6 +14,7 @@ use super::ExtractPayload; #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct FuncBackendManagementArgs { this_component: ComponentViewWithGeometry, + components: HashMap, } #[derive(Debug)] @@ -37,6 +40,7 @@ impl FuncDispatch for FuncBackendManagement { handler: handler.into(), code_base64: code_base64.into(), this_component: args.this_component, + components: args.components, before, }; diff --git a/lib/dal/src/func/binding.rs b/lib/dal/src/func/binding.rs index 6d687f0737..51587b643d 100644 --- a/lib/dal/src/func/binding.rs +++ b/lib/dal/src/func/binding.rs @@ -31,8 +31,8 @@ use crate::{ FuncError, FuncId, PropId, SchemaVariantError, SchemaVariantId, }; use crate::{ - ComponentId, InputSocketId, OutputSocketId, SchemaId, SchemaVariant, WorkspaceSnapshotError, - WsEventError, + ComponentId, InputSocketId, OutputSocketId, SchemaError, SchemaId, SchemaVariant, + WorkspaceSnapshotError, WsEventError, }; pub use attribute_argument::AttributeArgumentBinding; @@ -96,6 +96,8 @@ pub enum FuncBindingError { OutputSocket(#[from] OutputSocketError), #[error("prop error: {0}")] Prop(#[from] PropError), + #[error("schema error: {0}")] + Schema(#[from] SchemaError), #[error("schema variant error: {0}")] SchemaVariant(#[from] SchemaVariantError), #[error("serde error: {0}")] @@ -580,7 +582,9 @@ impl FuncBinding { LeafBinding::compile_leaf_func_types(ctx, func_id).await? } FuncKind::Attribute => AttributeBinding::compile_attribute_types(ctx, func_id).await?, - FuncKind::Management => String::new(), + FuncKind::Management => { + ManagementBinding::compile_management_types(ctx, func_id).await? + } FuncKind::Authentication | FuncKind::Intrinsic | FuncKind::SchemaVariantDefinition diff --git a/lib/dal/src/func/binding/management.rs b/lib/dal/src/func/binding/management.rs index 6e09df26ae..818845b65c 100644 --- a/lib/dal/src/func/binding/management.rs +++ b/lib/dal/src/func/binding/management.rs @@ -3,7 +3,7 @@ use telemetry::prelude::*; use crate::{ management::prototype::{ManagementPrototype, ManagementPrototypeId}, - DalContext, Func, FuncId, SchemaVariant, SchemaVariantId, + DalContext, Func, FuncId, Prop, Schema, SchemaVariant, SchemaVariantId, }; use super::{EventualParent, FuncBinding, FuncBindingResult}; @@ -44,6 +44,96 @@ impl ManagementBinding { FuncBinding::for_func_id(ctx, func_id).await } + pub async fn compile_management_types( + ctx: &DalContext, + func_id: FuncId, + ) -> FuncBindingResult { + let default_this_component = "object".to_string(); + let default_component_types = "object".to_string(); + let default_types = (default_this_component, default_component_types.to_owned()); + + let (this_component_iface, component_types) = + match ManagementPrototype::prototype_id_for_func_id(ctx, func_id).await? { + Some(prototype_id) => { + match ManagementPrototype::get_by_id(ctx, prototype_id).await? { + Some(prototype) => { + let variant_id = + ManagementPrototype::get_schema_variant_id(ctx, prototype_id) + .await?; + let root_prop = Prop::get_by_id( + ctx, + SchemaVariant::get_root_prop_id(ctx, variant_id).await?, + ) + .await?; + + let (_, reverse_map) = prototype.managed_schemas_map(ctx).await?; + + let mut component_types = vec![]; + for (schema_id, name) in reverse_map { + let variant_id = + Schema::get_or_install_default_variant(ctx, schema_id).await?; + + let root_prop = Prop::get_by_id( + ctx, + SchemaVariant::get_root_prop_id(ctx, variant_id).await?, + ) + .await?; + + let sv_type = root_prop.ts_type(ctx).await?; + + let component_type = format!( + r#" + {{ + kind: {name}, + properties: {sv_type}, + geometry: Geometry, + }} + "# + ); + component_types.push(component_type); + } + + let component_types = component_types.join("|\n"); + + (root_prop.ts_type(ctx).await?, component_types) + } + None => default_types, + } + } + None => default_types, + }; + + Ok(format!( + r#" +type Geometry = {{ + x: number, + y: number, + width?: number, + height?: number, +}}; +type Output = {{ + status: 'ok' | 'error'; + ops?: {{ + create?: {{ [key: string]: {component_types} }}, + update?: {{ [key: string]: {{ properties?: {{ [key: string]: unknown }}, geometry: Geometry, }} }}; + actions?: {{ [key: string]: {{ + add?: ("create" | "update" | "refresh" | "delete" | string)[]; + remove?: ("create" | "update" | "refresh" | "delete" | string)[]; + }} }} + + }}; + message?: string | null; +}}; +type Input = {{ + thisComponent: {{ + properties: {this_component_iface}, + geometry: Geometry, + }}, + components: {{ [key: string]: {component_types} }} +}};"# + )) + } + pub(crate) async fn assemble_management_bindings( ctx: &DalContext, func_id: FuncId, diff --git a/lib/dal/src/management/mod.rs b/lib/dal/src/management/mod.rs index b68234008f..8a83be6e0d 100644 --- a/lib/dal/src/management/mod.rs +++ b/lib/dal/src/management/mod.rs @@ -11,9 +11,11 @@ use crate::{ Action, ActionError, }, attribute::value::AttributeValueError, + change_status::ChangeStatus::Added, + diagram::geometry::RawGeometry, prop::{PropError, PropPath}, AttributeValue, Component, ComponentError, ComponentId, DalContext, Func, FuncError, Prop, - PropKind, WsEvent, WsEventError, + PropKind, Schema, SchemaError, SchemaId, WsEvent, WsEventError, }; pub mod prototype; @@ -26,6 +28,8 @@ pub enum ManagementError { ActionPrototype(#[from] ActionPrototypeError), #[error("attribute value error: {0}")] AttributeValue(#[from] AttributeValueError), + #[error("cannot create component with 'self' as a placeholder")] + CannotCreateComponentWithSelfPlaceholder, #[error("component error: {0}")] Component(#[from] ComponentError), #[error("cannot add an action of kind {0} because component {1} does not have an action of that kind")] @@ -34,20 +38,102 @@ pub enum ManagementError { "cannot add a manual action named {0} because component {1} does not have a manual action with that name" )] ComponentDoesNotHaveManualAction(String, ComponentId), + #[error("Component with management placeholder {0} could not be found")] + ComponentWithPlaceholderNotFound(String), + #[error("Duplicate component placeholder {0}")] + DuplicateComponentPlaceholder(String), #[error("func error: {0}")] Func(#[from] FuncError), #[error("prop error: {0}")] Prop(#[from] PropError), + #[error("schema error: {0}")] + Schema(#[from] SchemaError), + #[error("Cannot create component for Schema {0}, this schema does not exist or is not managed by this component")] + SchemaDoesNotExist(String), #[error("ws event error: {0}")] WsEvent(#[from] WsEventError), } pub type ManagementResult = Result; +#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq)] +pub struct NumericGeometry { + pub x: f64, + pub y: f64, + pub width: Option, + pub height: Option, +} + +impl NumericGeometry { + pub fn offset_by(&self, mut x_off: f64, mut y_off: f64) -> Self { + if !x_off.is_normal() { + x_off = 0.0; + } + if !y_off.is_normal() { + y_off = 0.0; + } + + let x = if self.x.is_normal() { + self.x + x_off + } else { + x_off + }; + + let y = if self.y.is_normal() { + self.y + y_off + } else { + y_off + }; + + Self { + x, + y, + width: self.width, + height: self.height, + } + } +} + +#[inline(always)] +fn avoid_nan_string(n: f64, fallback: f64) -> String { + if n.is_normal() { n.round() } else { fallback }.to_string() +} + +impl From for RawGeometry { + fn from(value: NumericGeometry) -> Self { + Self { + x: avoid_nan_string(value.x, 0.0), + y: avoid_nan_string(value.y, 0.0), + width: value.width.map(|w| avoid_nan_string(w, 500.0)), + height: value.height.map(|h| avoid_nan_string(h, 500.0)), + } + } +} + +impl From for NumericGeometry { + fn from(value: RawGeometry) -> Self { + Self { + x: value.x.parse().ok().unwrap_or(0.0), + y: value.y.parse().ok().unwrap_or(0.0), + width: value.width.and_then(|w| w.parse().ok()), + height: value.height.and_then(|h| h.parse().ok()), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManagementUpdateOperation { properties: Option, + geometry: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagementCreateOperation { + kind: Option, + properties: Option, + geometry: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -62,14 +148,15 @@ pub struct ManagementActionOperation { remove: Option>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManagementOperations { + create: Option>, update: Option>, actions: Option>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManagementFuncReturn { pub status: ManagementFuncStatus, @@ -96,51 +183,191 @@ impl TryFrom for ManagementFuncReturn { const SELF_ID: &str = "self"; -pub async fn operate( - ctx: &DalContext, +pub struct ManagementOperator<'a> { + ctx: &'a DalContext, manager_component_id: ComponentId, + last_component_geometry: NumericGeometry, operations: ManagementOperations, -) -> ManagementResult<()> { - // creation should be first + manager_schema_id: SchemaId, + schema_map: HashMap, + component_id_placeholders: HashMap, +} - if let Some(updates) = operations.update { - for (operation_component_id, operation) in updates { - // We only support operations on self right now - if operation_component_id != SELF_ID { - continue; - } +impl<'a> ManagementOperator<'a> { + pub async fn new( + ctx: &'a DalContext, + manager_component_id: ComponentId, + manager_component_geometry: RawGeometry, + operations: ManagementOperations, + schema_map: HashMap, + mut component_id_placeholders: HashMap, + ) -> ManagementResult { + component_id_placeholders.insert(SELF_ID.to_string(), manager_component_id); + + let manager_schema_id = Component::schema_for_component_id(ctx, manager_component_id) + .await? + .id(); + + Ok(Self { + ctx, + manager_component_id, + last_component_geometry: manager_component_geometry.into(), + operations, + manager_schema_id, + schema_map, + component_id_placeholders, + }) + } - let real_component_id = manager_component_id; + async fn create_component( + &self, + placeholder: &str, + operation: &ManagementCreateOperation, + ) -> ManagementResult<(ComponentId, NumericGeometry)> { + let schema_id = match &operation.kind { + Some(kind) => self + .schema_map + .get(kind) + .copied() + .ok_or(ManagementError::SchemaDoesNotExist(kind.clone()))?, + None => self.manager_schema_id, + }; + + let variant_id = Schema::get_or_install_default_variant(self.ctx, schema_id).await?; - if let Some(properties) = operation.properties { - update_component(ctx, real_component_id, properties).await?; + let mut component = Component::new(self.ctx, placeholder, variant_id).await?; + let geometry = if let Some(numeric_geometry) = &operation.geometry { + component + .set_raw_geometry(self.ctx, (*numeric_geometry).into()) + .await?; + + *numeric_geometry + } else { + // We don't want to just stack components on top of each other if no + // geometry is provided, so we're gonna do a bit of you-just-won + // solitaire staggering + let auto_geometry = self.last_component_geometry.offset_by(50.0, 50.0); + component + .set_raw_geometry(self.ctx, auto_geometry.into()) + .await?; + + auto_geometry + }; + + WsEvent::component_created( + self.ctx, + component + .into_frontend_type(self.ctx, Added, &mut HashMap::new()) + .await?, + ) + .await? + .publish_on_commit(self.ctx) + .await?; + + Ok((component.id(), geometry)) + } + + async fn creates(&mut self) -> ManagementResult<()> { + if let Some(creates) = &self.operations.create { + for (placeholder, operation) in creates { + if placeholder == SELF_ID { + return Err(ManagementError::CannotCreateComponentWithSelfPlaceholder); + } + + if self.component_id_placeholders.contains_key(placeholder) { + return Err(ManagementError::DuplicateComponentPlaceholder( + placeholder.to_owned(), + )); + } + + let (component_id, geometry) = + self.create_component(placeholder, operation).await?; + + self.last_component_geometry = geometry; + + self.component_id_placeholders + .insert(placeholder.to_owned(), component_id); + + if let Some(properties) = &operation.properties { + update_component( + self.ctx, + component_id, + properties, + &[&["root", "si", "name"]], + ) + .await?; + } + + Component::add_manages_edge_to_component( + self.ctx, + self.manager_component_id, + component_id, + crate::EdgeWeightKind::Manages, + ) + .await?; } } + + Ok(()) + } + + async fn get_real_component_id(&self, placeholder: &String) -> ManagementResult { + self.component_id_placeholders + .get(placeholder) + .copied() + .ok_or(ManagementError::ComponentWithPlaceholderNotFound( + placeholder.to_owned(), + )) } - if let Some(actions) = operations.actions { - for (operation_component_id, operations) in actions { - // We only support operations on self right now - if operation_component_id != SELF_ID { - continue; + async fn updates(&mut self) -> ManagementResult<()> { + if let Some(updates) = &self.operations.update { + for (placeholder, operation) in updates { + let component_id = self.get_real_component_id(placeholder).await?; + + if let Some(properties) = &operation.properties { + update_component(self.ctx, component_id, properties, &[]).await?; + } + if let Some(raw_geometry) = &operation.geometry { + let mut component = Component::get_by_id(self.ctx, component_id).await?; + component + .set_raw_geometry(self.ctx, raw_geometry.to_owned()) + .await?; + } } + } + + Ok(()) + } - let real_component_id = manager_component_id; + async fn actions(&self) -> ManagementResult<()> { + if let Some(actions) = &self.operations.actions { + for (placeholder, operations) in actions { + let component_id = self.get_real_component_id(placeholder).await?; + + operate_actions(self.ctx, component_id, operations).await?; + } - operate_actions(ctx, real_component_id, operations).await?; + WsEvent::action_list_updated(self.ctx) + .await? + .publish_on_commit(self.ctx) + .await?; } - WsEvent::action_list_updated(ctx) - .await? - .publish_on_commit(ctx) - .await?; + Ok(()) } - Ok(()) + pub async fn operate(&mut self) -> ManagementResult<()> { + self.creates().await?; + self.updates().await?; + self.actions().await?; + + Ok(()) + } } -fn identify_action(action_string: String) -> ActionIdentifier { - match action_string.as_str() { +fn identify_action(action_str: &str) -> ActionIdentifier { + match action_str { "create" => ActionIdentifier { kind: ActionKind::Create, manual_func_name: None, @@ -159,7 +386,7 @@ fn identify_action(action_string: String) -> ActionIdentifier { }, _ => ActionIdentifier { kind: ActionKind::Manual, - manual_func_name: Some(action_string), + manual_func_name: Some(action_str.to_string()), }, } } @@ -167,17 +394,23 @@ fn identify_action(action_string: String) -> ActionIdentifier { async fn operate_actions( ctx: &DalContext, component_id: ComponentId, - operation: ManagementActionOperation, + operation: &ManagementActionOperation, ) -> ManagementResult<()> { - if let Some(remove_actions) = operation.remove { - for to_remove in remove_actions.into_iter().map(identify_action) { + if let Some(remove_actions) = &operation.remove { + for to_remove in remove_actions + .iter() + .map(|action| identify_action(action.as_str())) + { remove_action(ctx, component_id, to_remove).await?; } } - if let Some(add_actions) = operation.add { + if let Some(add_actions) = &operation.add { let sv_id = Component::schema_variant_id(ctx, component_id).await?; let available_actions = ActionPrototype::for_variant(ctx, sv_id).await?; - for action in add_actions.into_iter().map(identify_action) { + for action in add_actions + .iter() + .map(|action| identify_action(action.as_str())) + { add_action(ctx, component_id, action, &available_actions).await?; } } @@ -292,7 +525,8 @@ const IGNORE_PATHS: [&[&str]; 6] = [ async fn update_component( ctx: &DalContext, component_id: ComponentId, - properties: serde_json::Value, + properties: &serde_json::Value, + extra_ignore_paths: &[&[&str]], ) -> ManagementResult<()> { let variant_id = Component::schema_variant_id(ctx, component_id).await?; @@ -302,7 +536,9 @@ async fn update_component( while let Some((path, current_val)) = work_queue.pop_front() { let path_as_refs: Vec<_> = path.iter().map(|part| part.as_str()).collect(); - if IGNORE_PATHS.contains(&path_as_refs.as_slice()) { + if IGNORE_PATHS.contains(&path_as_refs.as_slice()) + || extra_ignore_paths.contains(&path_as_refs.as_slice()) + { continue; } @@ -320,7 +556,8 @@ async fn update_component( continue; } if let serde_json::Value::Null = current_val { - AttributeValue::update(ctx, path_attribute_value_id, Some(current_val)).await?; + AttributeValue::update(ctx, path_attribute_value_id, Some(current_val.to_owned())) + .await?; continue; } @@ -333,8 +570,13 @@ async fn update_component( .await? .view(ctx) .await?; - if Some(¤t_val) != view.as_ref() { - AttributeValue::update(ctx, path_attribute_value_id, Some(current_val)).await?; + if Some(current_val) != view.as_ref() { + AttributeValue::update( + ctx, + path_attribute_value_id, + Some(current_val.to_owned()), + ) + .await?; } } PropKind::Object => { @@ -344,7 +586,7 @@ async fn update_component( for (key, value) in obj { let mut new_path = path.clone(); - new_path.push(key); + new_path.push(key.to_owned()); work_queue.push_back((new_path, value)); } } @@ -370,7 +612,7 @@ async fn update_component( // We do not descend below a map. Instead we update the *entire* // child tree of each map key for (key, value) in map { - match map_children.get(&key) { + match map_children.get(key) { Some(child_id) => { if AttributeValue::is_set_by_dependent_function(ctx, *child_id).await? { continue; @@ -379,16 +621,17 @@ async fn update_component( .await? .view(ctx) .await?; - if Some(&value) != view.as_ref() { - AttributeValue::update(ctx, *child_id, Some(value)).await?; + if Some(value) != view.as_ref() { + AttributeValue::update(ctx, *child_id, Some(value.to_owned())) + .await?; } } None => { AttributeValue::insert( ctx, path_attribute_value_id, - Some(value), - Some(key), + Some(value.to_owned()), + Some(key.to_owned()), ) .await?; } @@ -402,10 +645,14 @@ async fn update_component( .view(ctx) .await?; - if Some(¤t_val) != view.as_ref() { + if Some(current_val) != view.as_ref() { // Just update the entire array whole cloth - AttributeValue::update(ctx, path_attribute_value_id, Some(current_val)) - .await?; + AttributeValue::update( + ctx, + path_attribute_value_id, + Some(current_val.to_owned()), + ) + .await?; } } } diff --git a/lib/dal/src/management/prototype.rs b/lib/dal/src/management/prototype.rs index 4b5c1c828b..f6d757476f 100644 --- a/lib/dal/src/management/prototype.rs +++ b/lib/dal/src/management/prototype.rs @@ -1,25 +1,35 @@ //! A [`ManagementPrototype`] points to a Management [`Func`] for a schema variant -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use serde::{Deserialize, Serialize}; use si_events::FuncRunId; use thiserror::Error; -use veritech_client::{ComponentKind, ManagementResultSuccess}; +use veritech_client::ManagementResultSuccess; use crate::{ + cached_module::{CachedModule, CachedModuleError}, + diagram::geometry::RawGeometry, func::runner::{FuncRunner, FuncRunnerError}, id, implement_add_edge_to, layer_db_types::{ManagementPrototypeContent, ManagementPrototypeContentV1}, workspace_snapshot::node_weight::{traits::SiVersionedNodeWeight, NodeWeight}, Component, ComponentError, ComponentId, DalContext, EdgeWeightKind, - EdgeWeightKindDiscriminants, FuncId, HelperError, NodeWeightDiscriminants, SchemaId, - SchemaVariant, SchemaVariantError, SchemaVariantId, TransactionsError, WorkspaceSnapshotError, + EdgeWeightKindDiscriminants, FuncId, HelperError, NodeWeightDiscriminants, Schema, SchemaError, + SchemaId, SchemaVariant, SchemaVariantError, SchemaVariantId, TransactionsError, + WorkspaceSnapshotError, }; +use super::NumericGeometry; + #[remain::sorted] #[derive(Debug, Error)] pub enum ManagementPrototypeError { + #[error("cached module error: {0}")] + CachedModule(#[from] CachedModuleError), #[error("component error: {0}")] Component(#[from] ComponentError), #[error("func runner error: {0}")] @@ -32,6 +42,10 @@ pub enum ManagementPrototypeError { LayerDbError(#[from] si_layer_cache::LayerDbError), #[error("management prototype {0} has no use edge to a function")] MissingFunction(ManagementPrototypeId), + #[error("management prototype {0} not found")] + NotFound(ManagementPrototypeId), + #[error("schema error: {0}")] + Schema(#[from] SchemaError), #[error("schema variant error: {0}")] SchemaVariant(#[from] SchemaVariantError), #[error("serde json error: {0}")] @@ -59,13 +73,105 @@ impl From for si_events::ManagementPrototypeId { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ManagementPrototype { - pub id: ManagementPrototypeId, - pub managed_schemas: Option>, - pub name: String, - pub description: Option, + id: ManagementPrototypeId, + managed_schemas: Option>, + name: String, + description: Option, +} + +#[derive(Debug, Clone)] +pub struct ManagementPrototypeExecution { + pub func_run_id: FuncRunId, + pub result: Option, + pub manager_component_geometry: RawGeometry, + pub managed_schema_map: HashMap, + pub placeholders: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ManagedComponent { + kind: String, + properties: Option, + geometry: NumericGeometry, } impl ManagementPrototype { + pub fn id(&self) -> ManagementPrototypeId { + self.id + } + + pub fn managed_schemas(&self) -> Option<&HashSet> { + self.managed_schemas.as_ref() + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + + pub async fn schema_id(&self, ctx: &DalContext) -> ManagementPrototypeResult> { + let snapshot = ctx.workspace_snapshot()?; + + let Some(sv_source_idx) = snapshot + .incoming_sources_for_edge_weight_kind( + self.id, + EdgeWeightKindDiscriminants::ManagementPrototype, + ) + .await? + .first() + .copied() + else { + return Ok(None); + }; + + let sv_id = snapshot.get_node_weight(sv_source_idx).await?.id(); + + Ok(Some( + SchemaVariant::schema_id_for_schema_variant_id(ctx, sv_id.into()).await?, + )) + } + + /// Generates a map between the schema name and its schema id (even for + /// uninstalled schemas), and a reverse mapping. These names will be + /// provided to the management executor and operator so that management + /// functions can create components of specific schema kinds. + pub async fn managed_schemas_map( + &self, + ctx: &DalContext, + ) -> ManagementPrototypeResult<(HashMap, HashMap)> { + let mut managed_schemas_map = HashMap::new(); + let mut reverse_map = HashMap::new(); + + let mut managed_schemas = self.managed_schemas().cloned().unwrap_or_default(); + if let Some(schema_id) = self.schema_id(ctx).await? { + managed_schemas.insert(schema_id); + } + + for schema_id in managed_schemas { + let schema_name = match Schema::get_by_id(ctx, schema_id).await? { + Some(schema) => schema.name().to_owned(), + None => { + let Some(cached_module) = + CachedModule::latest_by_schema_id(ctx, schema_id).await? + else { + continue; + }; + + cached_module.schema_name + } + }; + + managed_schemas_map.insert(schema_name.clone(), schema_id); + reverse_map.insert(schema_id, schema_name); + } + + Ok((managed_schemas_map, reverse_map)) + } + pub async fn new( ctx: &DalContext, name: String, @@ -176,7 +282,7 @@ impl ManagementPrototype { &self, ctx: &DalContext, manager_component_id: ComponentId, - ) -> ManagementPrototypeResult<(Option, FuncRunId)> { + ) -> ManagementPrototypeResult { Self::execute_by_id(ctx, self.id, manager_component_id).await } @@ -184,18 +290,53 @@ impl ManagementPrototype { ctx: &DalContext, id: ManagementPrototypeId, manager_component_id: ComponentId, - ) -> ManagementPrototypeResult<(Option, FuncRunId)> { + ) -> ManagementPrototypeResult { + let prototype = Self::get_by_id(ctx, id) + .await? + .ok_or(ManagementPrototypeError::NotFound(id))?; + + let (managed_schema_map, reverse_map) = prototype.managed_schemas_map(ctx).await?; + let management_func_id = ManagementPrototype::func_id(ctx, id).await?; let manager_component = Component::get_by_id(ctx, manager_component_id).await?; let manager_component_view = manager_component.view(ctx).await?; - let geometry = manager_component.geometry(ctx).await?; + let geometry = manager_component.geometry(ctx).await?.into_raw(); + + let managed_schema_names: Vec = managed_schema_map.keys().cloned().collect(); + + let mut managed_components = HashMap::new(); + for component_id in manager_component.get_managed(ctx).await? { + let component = Component::get_by_id(ctx, component_id).await?; + let component_view = component.view(ctx).await?; + let component_geometry: NumericGeometry = + component.geometry(ctx).await?.into_raw().into(); + let schema_id = component.schema(ctx).await?.id(); + + if let Some(managed_schema_name) = reverse_map.get(&schema_id) { + managed_components.insert( + component_id, + ManagedComponent { + kind: managed_schema_name.clone(), + properties: component_view, + geometry: component_geometry, + }, + ); + } + } + + let placeholders: HashMap<_, _> = managed_components + .keys() + .copied() + .map(|id| (id.to_string(), id)) + .collect(); let args = serde_json::json!({ "this_component": { - "kind": ComponentKind::Standard, "properties": manager_component_view, - "geometry": geometry, - } + "geometry": geometry.to_owned(), + }, + "managed_schemas": managed_schema_names, + "components": managed_components, }); let result_channel = @@ -241,7 +382,13 @@ impl ManagementPrototype { None => None, }; - Ok((maybe_run_result, func_run_id)) + Ok(ManagementPrototypeExecution { + func_run_id, + result: maybe_run_result, + manager_component_geometry: geometry, + managed_schema_map, + placeholders, + }) } pub async fn get_schema_variant_id( @@ -333,6 +480,30 @@ impl ManagementPrototype { Ok(()) } + pub async fn prototype_id_for_func_id( + ctx: &DalContext, + func_id: FuncId, + ) -> ManagementPrototypeResult> { + let workspace_snapshot = ctx.workspace_snapshot()?; + let use_sources = workspace_snapshot + .incoming_sources_for_edge_weight_kind(func_id, EdgeWeightKindDiscriminants::Use) + .await?; + + for use_source in use_sources { + let node_weight = workspace_snapshot.get_node_weight(use_source).await?; + let node_weight_id = node_weight.id(); + if NodeWeightDiscriminants::ManagementPrototype == node_weight.into() { + if let Some(management_prototype) = + Self::get_by_id(ctx, node_weight_id.into()).await? + { + return Ok(Some(management_prototype.id)); + } + } + } + + Ok(None) + } + implement_add_edge_to!( source_id: ManagementPrototypeId, destination_id: FuncId, diff --git a/lib/dal/src/pkg/export.rs b/lib/dal/src/pkg/export.rs index bf168e4214..7dd29db36c 100644 --- a/lib/dal/src/pkg/export.rs +++ b/lib/dal/src/pkg/export.rs @@ -13,7 +13,6 @@ use si_pkg::{ SiPropFuncSpecKind, SocketSpec, SocketSpecData, SocketSpecKind, SpecError, }; use telemetry::prelude::*; -use ulid::Ulid; use crate::action::prototype::ActionPrototype; use crate::attribute::prototype::argument::{ @@ -519,7 +518,7 @@ impl PkgExporter { ManagementPrototype::list_for_variant_id(ctx, schema_variant_id).await?; for management_proto in management_prototypes { - let key = ManagementPrototype::func_id(ctx, management_proto.id).await?; + let key = ManagementPrototype::func_id(ctx, management_proto.id()).await?; let func_spec = self .func_map @@ -527,19 +526,19 @@ impl PkgExporter { .ok_or(PkgError::MissingExportedFunc(key))?; let mut builder = ManagementFuncSpec::builder(); - if let Some(description) = management_proto.description { - builder.description(description); + if let Some(description) = management_proto.description() { + builder.description(description.to_string()); } - if let Some(managed_schemas) = management_proto.managed_schemas { - let managed_schemas: HashSet = - managed_schemas.into_iter().map(Into::into).collect(); + if let Some(managed_schemas) = management_proto.managed_schemas() { + let managed_schemas: HashSet<_> = + managed_schemas.iter().map(|id| id.to_string()).collect(); builder.managed_schemas(managed_schemas); } specs.push( builder .func_unique_id(&func_spec.unique_id) - .name(management_proto.name) + .name(management_proto.name()) .build()?, ) } diff --git a/lib/dal/src/pkg/import.rs b/lib/dal/src/pkg/import.rs index 60f37f5a74..ae51c57228 100644 --- a/lib/dal/src/pkg/import.rs +++ b/lib/dal/src/pkg/import.rs @@ -853,9 +853,13 @@ async fn import_management_func( management_func_spec.name().to_string(), management_func_spec.description().map(Into::into), func_id, - management_func_spec - .managed_schemas() - .map(|schemas| schemas.iter().map(|ulid| (*ulid).into()).collect()), + management_func_spec.managed_schemas().map(|schemas| { + schemas + .iter() + .filter_map(|ulid| Ulid::from_string(ulid).ok()) + .map(Into::into) + .collect() + }), schema_variant_id, ) .await?; diff --git a/lib/dal/src/prop.rs b/lib/dal/src/prop.rs index f7522c2953..9102feea1c 100644 --- a/lib/dal/src/prop.rs +++ b/lib/dal/src/prop.rs @@ -1133,7 +1133,6 @@ impl Prop { Self::find_prop_id_by_path(ctx, schema_variant_id, &prop_path).await } - #[instrument(level = "debug", skip_all)] #[async_recursion] pub async fn ts_type(&self, ctx: &DalContext) -> PropResult { let self_path = self.path(ctx).await?; diff --git a/lib/dal/src/schema.rs b/lib/dal/src/schema.rs index 247b0e96c8..e3b9f605c1 100644 --- a/lib/dal/src/schema.rs +++ b/lib/dal/src/schema.rs @@ -9,8 +9,10 @@ use telemetry::prelude::*; use thiserror::Error; use tokio::sync::TryLockError; +use crate::cached_module::{CachedModule, CachedModuleError}; use crate::change_set::ChangeSetError; use crate::layer_db_types::{SchemaContent, SchemaContentDiscriminants, SchemaContentV1}; +use crate::pkg::{import_pkg_from_pkg, ImportOptions, PkgError}; use crate::workspace_snapshot::content_address::{ContentAddress, ContentAddressDiscriminants}; use crate::workspace_snapshot::edge_weight::{ EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants, @@ -33,6 +35,8 @@ pub const SCHEMA_VERSION: SchemaContentDiscriminants = SchemaContentDiscriminant #[remain::sorted] #[derive(Error, Debug)] pub enum SchemaError { + #[error("cached module error: {0}")] + CachedModule(#[from] CachedModuleError), #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), #[error("func error: {0}")] @@ -41,14 +45,20 @@ pub enum SchemaError { Helper(#[from] HelperError), #[error("layer db error: {0}")] LayerDb(#[from] LayerDbError), + #[error("No default schema variant exists for {0}")] + NoDefaultSchemaVariant(SchemaId), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), + #[error("pkg error: {0}")] + Pkg(#[from] Box), #[error("schema variant error: {0}")] SchemaVariant(#[from] Box), #[error("transactions error: {0}")] Transactions(#[from] TransactionsError), #[error("try lock error: {0}")] TryLock(#[from] TryLockError), + #[error("uninstalled schema {0} not found")] + UninstalledSchemaNotFound(SchemaId), #[error("workspace snapshot error: {0}")] WorkspaceSnapshot(#[from] WorkspaceSnapshotError), } @@ -520,4 +530,41 @@ impl Schema { pub async fn is_name_taken(ctx: &DalContext, name: &String) -> SchemaResult { Ok(Self::list(ctx).await?.iter().any(|s| s.name.eq(name))) } + + /// Returns the default [`SchemaVariantId`] for the provided [`SchemaId`] + /// *if* this schema is installed. If this schema is not installed, it looks + /// for it in the local module cache, and if it exists there, it installs it, then + /// returns the newly installed default [`SchemaVariantId`]. + pub async fn get_or_install_default_variant( + ctx: &DalContext, + schema_id: SchemaId, + ) -> SchemaResult { + Ok(match Schema::get_by_id(ctx, schema_id).await? { + Some(schema) => schema + .get_default_schema_variant_id(ctx) + .await? + .ok_or(SchemaError::NoDefaultSchemaVariant(schema_id))?, + None => { + let mut uninstalled_module = CachedModule::latest_by_schema_id(ctx, schema_id) + .await? + .ok_or(SchemaError::UninstalledSchemaNotFound(schema_id))?; + + let si_pkg = uninstalled_module.si_pkg(ctx).await?; + import_pkg_from_pkg( + ctx, + &si_pkg, + Some(ImportOptions { + schema_id: Some(schema_id.into()), + ..Default::default() + }), + ) + .await + .map_err(Box::new)?; + + Schema::get_default_schema_variant_by_id(ctx, schema_id) + .await? + .ok_or(SchemaError::NoDefaultSchemaVariant(schema_id))? + } + }) + } } diff --git a/lib/dal/src/schema/variant.rs b/lib/dal/src/schema/variant.rs index baeddc23dc..68a3a6af61 100644 --- a/lib/dal/src/schema/variant.rs +++ b/lib/dal/src/schema/variant.rs @@ -29,7 +29,9 @@ use crate::layer_db_types::{ ContentTypeError, InputSocketContent, OutputSocketContent, SchemaVariantContent, SchemaVariantContentV3, }; -use crate::management::prototype::{ManagementPrototype, ManagementPrototypeId}; +use crate::management::prototype::{ + ManagementPrototype, ManagementPrototypeError, ManagementPrototypeId, +}; use crate::module::Module; use crate::prop::{PropError, PropPath}; use crate::schema::variant::root_prop::RootProp; @@ -120,8 +122,8 @@ pub enum SchemaVariantError { LeafFunctionMustBeJsAttribute(FuncId), #[error("Leaf map prop not found for item prop {0}")] LeafMapPropNotFound(PropId), - #[error("schema variant missing management func id; schema_variant_id={0}")] - ManagementPrototype(String), + #[error("management prototype error: {0}")] + ManagementPrototype(#[from] Box), #[error("schema variant missing asset func id; schema_variant_id={0}")] MissingAssetFuncId(SchemaVariantId), #[error("module error: {0}")] @@ -1950,7 +1952,7 @@ impl SchemaVariant { let func_id = ManagementPrototype::func_id(ctx, mgmt_prototype_node_weight.id().into()) .await - .map_err(|err| SchemaVariantError::ManagementPrototype(err.to_string()))?; + .map_err(Box::new)?; all_func_ids.insert(func_id); } @@ -2243,7 +2245,8 @@ impl SchemaVariant { | EdgeWeightKindDiscriminants::Proxy | EdgeWeightKindDiscriminants::Root | EdgeWeightKindDiscriminants::SocketValue - | EdgeWeightKindDiscriminants::ValidationOutput => {} + | EdgeWeightKindDiscriminants::ValidationOutput + | EdgeWeightKindDiscriminants::Manages => {} } } @@ -2349,4 +2352,23 @@ impl SchemaVariant { Ok(schema_variants.into_values().collect()) } + + /// Gathers all the schemas managed by the management prototypes for a given [`SchemaVariant`](SchemaVariant). + pub async fn all_managed_schemas( + ctx: &DalContext, + schema_variant_id: SchemaVariantId, + ) -> SchemaVariantResult> { + let mut result = HashSet::new(); + + for prototype in ManagementPrototype::list_for_variant_id(ctx, schema_variant_id) + .await + .map_err(Box::new)? + { + if let Some(managed_schemas) = prototype.managed_schemas() { + result.extend(managed_schemas); + } + } + + Ok(result) + } } diff --git a/lib/dal/src/workspace_snapshot/edge_weight.rs b/lib/dal/src/workspace_snapshot/edge_weight.rs index cf9e7dd465..47c0057b8b 100644 --- a/lib/dal/src/workspace_snapshot/edge_weight.rs +++ b/lib/dal/src/workspace_snapshot/edge_weight.rs @@ -65,6 +65,8 @@ pub enum EdgeWeightKind { ManagementPrototype, /// From a Geometry node to the node it represents on a view. Represents, + /// From a manager [`Component`][`crate::Component`] to a managed `Component + Manages, } impl EdgeWeightKind { diff --git a/lib/dal/src/workspace_snapshot/graph/v2.rs b/lib/dal/src/workspace_snapshot/graph/v2.rs index e1469bcc45..a9ed10d0e1 100644 --- a/lib/dal/src/workspace_snapshot/graph/v2.rs +++ b/lib/dal/src/workspace_snapshot/graph/v2.rs @@ -1233,7 +1233,8 @@ impl WorkspaceSnapshotGraphV2 { | EdgeWeightKind::Root | EdgeWeightKind::SocketValue | EdgeWeightKind::ManagementPrototype - | EdgeWeightKind::ValidationOutput => {} + | EdgeWeightKind::ValidationOutput + | EdgeWeightKind::Manages => {} } } } diff --git a/lib/dal/src/workspace_snapshot/graph/v3.rs b/lib/dal/src/workspace_snapshot/graph/v3.rs index b95309f18f..0f0050cfce 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3.rs +++ b/lib/dal/src/workspace_snapshot/graph/v3.rs @@ -749,6 +749,7 @@ impl WorkspaceSnapshotGraphV3 { EdgeWeightKindDiscriminants::Use => "black", EdgeWeightKindDiscriminants::ValidationOutput => "darkcyan", EdgeWeightKindDiscriminants::ManagementPrototype => "pink", + EdgeWeightKindDiscriminants::Manages => "pink", }; match edgeref.weight().kind() { @@ -1409,7 +1410,8 @@ impl WorkspaceSnapshotGraphV3 { | EdgeWeightKind::Root | EdgeWeightKind::SocketValue | EdgeWeightKind::ValidationOutput - | EdgeWeightKind::ManagementPrototype => {} + | EdgeWeightKind::ManagementPrototype + | EdgeWeightKind::Manages => {} } } } diff --git a/lib/dal/src/workspace_snapshot/graph/v4.rs b/lib/dal/src/workspace_snapshot/graph/v4.rs index 6af548e0fc..406cba06e2 100644 --- a/lib/dal/src/workspace_snapshot/graph/v4.rs +++ b/lib/dal/src/workspace_snapshot/graph/v4.rs @@ -862,6 +862,7 @@ impl WorkspaceSnapshotGraphV4 { EdgeWeightKindDiscriminants::Use => "black", EdgeWeightKindDiscriminants::ValidationOutput => "darkcyan", EdgeWeightKindDiscriminants::ManagementPrototype => "pink", + EdgeWeightKindDiscriminants::Manages => "pink", }; match edgeref.weight().kind() { @@ -1530,7 +1531,8 @@ impl WorkspaceSnapshotGraphV4 { | EdgeWeightKind::Root | EdgeWeightKind::SocketValue | EdgeWeightKind::ValidationOutput - | EdgeWeightKind::ManagementPrototype => {} + | EdgeWeightKind::ManagementPrototype + | EdgeWeightKind::Manages => {} } } } diff --git a/lib/dal/tests/integration_test/deserialize/mod.rs b/lib/dal/tests/integration_test/deserialize/mod.rs index f44761ac4c..f5ec093048 100644 --- a/lib/dal/tests/integration_test/deserialize/mod.rs +++ b/lib/dal/tests/integration_test/deserialize/mod.rs @@ -189,6 +189,7 @@ fn make_me_one_with_everything(graph: &mut WorkspaceSnapshotGraphVCurrent) { EdgeWeightKindDiscriminants::ValidationOutput => EdgeWeightKind::ValidationOutput, EdgeWeightKindDiscriminants::ManagementPrototype => EdgeWeightKind::ManagementPrototype, EdgeWeightKindDiscriminants::Represents => EdgeWeightKind::Represents, + EdgeWeightKindDiscriminants::Manages => EdgeWeightKind::Manages, }; let edge_weight = EdgeWeight::new(edge_weight_kind); diff --git a/lib/dal/tests/integration_test/func/kill_execution.rs b/lib/dal/tests/integration_test/func/kill_execution.rs index 386217957b..1de447fe04 100644 --- a/lib/dal/tests/integration_test/func/kill_execution.rs +++ b/lib/dal/tests/integration_test/func/kill_execution.rs @@ -37,7 +37,7 @@ async fn kill_execution_works(ctx: &mut DalContext) { .await .expect("could new leaf func"); let code = "async function main() { - const ms = 480 * 1000; + const ms = 600 * 1000; const sleep = new Promise((resolve) => setTimeout(resolve, ms)); await sleep; return { payload: { \"poop\": true }, status: \"ok\" }; diff --git a/lib/dal/tests/integration_test/management.rs b/lib/dal/tests/integration_test/management.rs index 2a3d6f2c7d..3fb710139c 100644 --- a/lib/dal/tests/integration_test/management.rs +++ b/lib/dal/tests/integration_test/management.rs @@ -1,10 +1,304 @@ +use std::collections::HashMap; + use dal::{ - management::{operate, prototype::ManagementPrototype, ManagementFuncReturn}, + management::{ + prototype::ManagementPrototype, ManagementFuncReturn, ManagementOperator, NumericGeometry, + }, AttributeValue, Component, DalContext, }; use dal_test::{helpers::create_component_for_default_schema_name, test}; use veritech_client::ManagementFuncStatus; +#[test] +async fn update_managed_components(ctx: &DalContext) { + let small_odd_lego = + create_component_for_default_schema_name(ctx, "small odd lego", "small odd lego") + .await + .expect("could not create component"); + let small_even_lego = + create_component_for_default_schema_name(ctx, "small even lego", "small even lego") + .await + .expect("could not create component"); + + Component::manage_component(ctx, small_odd_lego.id(), small_even_lego.id()) + .await + .expect("add manages edge"); + + let manager_variant = small_odd_lego + .schema_variant(ctx) + .await + .expect("get variant"); + + let management_prototype = ManagementPrototype::list_for_variant_id(ctx, manager_variant.id()) + .await + .expect("get prototypes") + .into_iter() + .find(|proto| proto.name() == "Update") + .expect("could not find prototype"); + + let execution_result = management_prototype + .execute(ctx, small_odd_lego.id()) + .await + .expect("should execute management prototype func"); + + let result: ManagementFuncReturn = execution_result + .result + .expect("should have a result success") + .try_into() + .expect("should be a valid management func return"); + + assert_eq!(result.status, ManagementFuncStatus::Ok); + + let operations = result.operations.expect("should have operations"); + + ManagementOperator::new( + ctx, + small_odd_lego.id(), + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + execution_result.placeholders, + ) + .await + .expect("should create operator") + .operate() + .await + .expect("should operate"); + + let mut new_component = None; + let components = Component::list(ctx).await.expect("list components"); + assert_eq!(2, components.len()); + for c in components { + if c.name(ctx).await.expect("get name") == "small even lego managed by small odd lego" { + new_component = Some(c); + break; + } + } + + let _new_component = new_component.expect("should have found the cloned component"); +} + +#[test] +async fn create_component_of_other_schema(ctx: &DalContext) { + let small_odd_lego = + create_component_for_default_schema_name(ctx, "small odd lego", "small odd lego") + .await + .expect("could not create component"); + let small_even_lego = + create_component_for_default_schema_name(ctx, "small even lego", "small even lego") + .await + .expect("could not create component"); + + let av_id = Component::attribute_value_for_prop_by_id( + ctx, + small_even_lego.id(), + &["root", "si", "resourceId"], + ) + .await + .expect("av should exist"); + + AttributeValue::update( + ctx, + av_id, + Some(serde_json::json!("small even lego resource id")), + ) + .await + .expect("able to update value"); + + Component::manage_component(ctx, small_odd_lego.id(), small_even_lego.id()) + .await + .expect("add manages edge"); + + let manager_variant = small_odd_lego + .schema_variant(ctx) + .await + .expect("get variant"); + + let management_prototype = ManagementPrototype::list_for_variant_id(ctx, manager_variant.id()) + .await + .expect("get prototypes") + .into_iter() + .find(|proto| proto.name() == "Clone") + .expect("could not find prototype"); + + let execution_result = management_prototype + .execute(ctx, small_odd_lego.id()) + .await + .expect("should execute management prototype func"); + + let result: ManagementFuncReturn = execution_result + .result + .expect("should have a result success") + .try_into() + .expect("should be a valid management func return"); + + assert_eq!(result.status, ManagementFuncStatus::Ok); + + let operations = result.operations.expect("should have operations"); + + ManagementOperator::new( + ctx, + small_odd_lego.id(), + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + execution_result.placeholders, + ) + .await + .expect("should create operator") + .operate() + .await + .expect("should operate"); + + let mut new_component = None; + let components = Component::list(ctx).await.expect("list components"); + assert_eq!(4, components.len()); + for c in components { + if c.name(ctx).await.expect("get name") == "small even lego_clone" { + new_component = Some(c); + break; + } + } + + let new_component = new_component.expect("should have found the cloned component"); + let av_id = Component::attribute_value_for_prop_by_id( + ctx, + new_component.id(), + &["root", "si", "resourceId"], + ) + .await + .expect("av should exist"); + + let av = AttributeValue::get_by_id(ctx, av_id) + .await + .expect("get value"); + + assert_eq!( + Some(serde_json::json!("small even lego resource id")), + av.value(ctx).await.expect("get value") + ); +} + +#[test] +async fn create_component_of_same_schema(ctx: &DalContext) { + let small_odd_lego = + create_component_for_default_schema_name(ctx, "small odd lego", "small odd lego") + .await + .expect("could not create component"); + let variant = small_odd_lego + .schema_variant(ctx) + .await + .expect("get variant"); + + let management_prototype = ManagementPrototype::list_for_variant_id(ctx, variant.id()) + .await + .expect("get prototypes") + .into_iter() + .find(|proto| proto.name() == "Clone") + .expect("could not find prototype"); + + let execution_result = management_prototype + .execute(ctx, small_odd_lego.id()) + .await + .expect("should execute management prototype func"); + + let result: ManagementFuncReturn = execution_result + .result + .expect("should have a result success") + .try_into() + .expect("should be a valid management func return"); + + assert_eq!(result.status, ManagementFuncStatus::Ok); + + let operations = result.operations.expect("should have operations"); + + ManagementOperator::new( + ctx, + small_odd_lego.id(), + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + execution_result.placeholders, + ) + .await + .expect("should create operator") + .operate() + .await + .expect("should operate"); + + let mut new_component = None; + let components = Component::list(ctx).await.expect("list components"); + assert_eq!(2, components.len()); + for c in components { + if c.name(ctx).await.expect("get name") == "small odd lego_clone" { + new_component = Some(c); + break; + } + } + + let new_component = new_component.expect("should have found the cloned component"); + let new_geometry: NumericGeometry = new_component + .geometry(ctx) + .await + .expect("get geometry") + .into_raw() + .into(); + + assert_eq!(10.0, new_geometry.x); + assert_eq!(20.0, new_geometry.y); + + let managers = new_component.get_managers(ctx).await.expect("get managers"); + + assert_eq!(1, managers.len()); + assert_eq!( + small_odd_lego.id(), + managers[0], + "should have the same manager" + ); + + let execution_result = management_prototype + .execute(ctx, small_odd_lego.id()) + .await + .expect("should execute management prototype func"); + + let result: ManagementFuncReturn = execution_result + .result + .expect("should have a result success") + .try_into() + .expect("should be a valid management func return"); + + assert_eq!(result.status, ManagementFuncStatus::Ok); + + let operations = result.operations.expect("should have operations"); + + ManagementOperator::new( + ctx, + small_odd_lego.id(), + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + HashMap::new(), + ) + .await + .expect("should create operator") + .operate() + .await + .expect("should operate"); + + let mut new_component_2 = None; + + let components = Component::list(ctx).await.expect("list components"); + assert_eq!(4, components.len()); + for c in components { + let name = c.name(ctx).await.expect("get name"); + if name == "small odd lego_clone_clone" { + new_component_2 = Some(c); + //break; + } + } + let _new_component_2 = new_component_2.expect("should have found the cloned component again"); +} + #[test] async fn execute_management_func(ctx: &DalContext) { let small_odd_lego = @@ -32,27 +326,36 @@ async fn execute_management_func(ctx: &DalContext) { .await .expect("get prototypes") .into_iter() - .find(|proto| proto.name == "Import small odd lego") + .find(|proto| proto.name() == "Import small odd lego") .expect("could not find prototype"); - let (result_value, _) = management_prototype + let execution_result = management_prototype .execute(ctx, small_odd_lego.id()) .await .expect("should execute management prototype func"); - let result: ManagementFuncReturn = result_value + let result: ManagementFuncReturn = execution_result + .result .expect("should have a result success") .try_into() .expect("should be a valid management func return"); assert_eq!(result.status, ManagementFuncStatus::Ok); - operate( + let operations = result.operations.expect("should have operations"); + + ManagementOperator::new( ctx, small_odd_lego.id(), - result.operations.expect("should have operations"), + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + execution_result.placeholders, ) .await + .expect("should create operator") + .operate() + .await .expect("should operate"); let av_id = Component::attribute_value_for_prop_by_id( diff --git a/lib/sdf-server/src/service/diagram.rs b/lib/sdf-server/src/service/diagram.rs index 807a86ad37..36078da056 100644 --- a/lib/sdf-server/src/service/diagram.rs +++ b/lib/sdf-server/src/service/diagram.rs @@ -103,8 +103,6 @@ pub enum DiagramError { Schema(#[from] SchemaError), #[error("schema not found")] SchemaNotFound, - #[error("No schema installed after successful package import for {0}")] - SchemaNotInstalledAfterImport(SchemaId), #[error("serde error: {0}")] Serde(#[from] serde_json::Error), #[error("slow runtime error: {0}")] diff --git a/lib/sdf-server/src/service/diagram/create_component.rs b/lib/sdf-server/src/service/diagram/create_component.rs index afe141bb43..bb6e80182b 100644 --- a/lib/sdf-server/src/service/diagram/create_component.rs +++ b/lib/sdf-server/src/service/diagram/create_component.rs @@ -7,13 +7,8 @@ use axum::{ use serde::{Deserialize, Serialize}; use dal::{ - cached_module::CachedModule, - change_status::ChangeStatus, - component::frame::Frame, - generate_name, - pkg::{import_pkg_from_pkg, ImportOptions}, - ChangeSet, Component, ComponentId, Schema, SchemaId, SchemaVariant, SchemaVariantId, - Visibility, WsEvent, + change_status::ChangeStatus, component::frame::Frame, generate_name, ChangeSet, Component, + ComponentId, Schema, SchemaId, SchemaVariant, SchemaVariantId, Visibility, WsEvent, }; use si_events::audit_log::AuditLogKind; use si_frontend_types::SchemaVariant as FrontendVariant; @@ -84,37 +79,14 @@ pub async fn create_component( "schemaId missing on uninstalled schema create component request".into(), ))?; - let variant_id = match Schema::get_by_id(&ctx, schema_id).await? { - // We want to be sure that we don't have stale frontend data, - // since this module might have just been installed, or - // installed by another user - Some(schema) => schema - .get_default_schema_variant_id(&ctx) - .await? - .ok_or(DiagramError::NoDefaultSchemaVariant(schema_id))?, - None => { - let mut uninstalled_module = CachedModule::latest_by_schema_id(&ctx, schema_id) - .await? - .ok_or(DiagramError::UninstalledSchemaNotFound(schema_id))?; - - let si_pkg = uninstalled_module.si_pkg(&ctx).await?; - import_pkg_from_pkg( - &ctx, - &si_pkg, - Some(ImportOptions { - schema_id: Some(schema_id.into()), - ..Default::default() - }), - ) - .await?; - - Schema::get_default_schema_variant_by_id(&ctx, schema_id) - .await? - .ok_or(DiagramError::SchemaNotInstalledAfterImport(schema_id))? - } - }; - + let variant_id = Schema::get_or_install_default_variant(&ctx, schema_id).await?; let variant = SchemaVariant::get_by_id_or_error(&ctx, variant_id).await?; + let managed_schema_ids = SchemaVariant::all_managed_schemas(&ctx, variant_id).await?; + + // Also install any schemas managed by the variant + for schema_id in managed_schema_ids { + Schema::get_or_install_default_variant(&ctx, schema_id).await?; + } ( variant_id, diff --git a/lib/sdf-server/src/service/v2/management.rs b/lib/sdf-server/src/service/v2/management.rs index 0c69daeaba..1f55675e62 100644 --- a/lib/sdf-server/src/service/v2/management.rs +++ b/lib/sdf-server/src/service/v2/management.rs @@ -11,9 +11,8 @@ use axum::{ }; use dal::{ management::{ - operate, prototype::{ManagementPrototype, ManagementPrototypeError, ManagementPrototypeId}, - ManagementError, ManagementFuncReturn, + ManagementError, ManagementFuncReturn, ManagementOperator, }, ChangeSet, ChangeSetError, ChangeSetId, ComponentId, TransactionsError, WorkspacePk, }; @@ -81,7 +80,8 @@ pub async fn run_prototype( let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; // TODO check that this is a valid prototypeId - let (result, _) = ManagementPrototype::execute_by_id(&ctx, prototype_id, component_id).await?; + let execution_result = + ManagementPrototype::execute_by_id(&ctx, prototype_id, component_id).await?; track( &posthog_client, @@ -96,11 +96,21 @@ pub async fn run_prototype( }), ); - if let Some(result) = result { + if let Some(result) = execution_result.result { let result: ManagementFuncReturn = result.try_into()?; if result.status == ManagementFuncStatus::Ok { if let Some(operations) = result.operations { - operate(&ctx, component_id, operations).await?; + ManagementOperator::new( + &ctx, + component_id, + execution_result.manager_component_geometry, + operations, + execution_result.managed_schema_map, + execution_result.placeholders, + ) + .await? + .operate() + .await?; } } diff --git a/lib/si-pkg/src/node/management_func.rs b/lib/si-pkg/src/node/management_func.rs index 37ea1bd606..d8cb1bfeca 100644 --- a/lib/si-pkg/src/node/management_func.rs +++ b/lib/si-pkg/src/node/management_func.rs @@ -7,7 +7,6 @@ use object_tree::{ read_key_value_line, read_key_value_line_opt, write_key_value_line, write_key_value_line_opt, GraphError, NodeChild, NodeKind, NodeWithChildren, ReadBytes, WriteBytes, }; -use ulid::Ulid; use crate::ManagementFuncSpec; @@ -23,7 +22,7 @@ pub struct ManagementFuncNode { pub func_unique_id: String, pub name: String, pub description: Option, - pub managed_schemas: Option>, + pub managed_schemas: Option>, } impl WriteBytes for ManagementFuncNode { diff --git a/lib/si-pkg/src/pkg/management_func.rs b/lib/si-pkg/src/pkg/management_func.rs index 42c8f65d8c..8cd9b71699 100644 --- a/lib/si-pkg/src/pkg/management_func.rs +++ b/lib/si-pkg/src/pkg/management_func.rs @@ -2,7 +2,6 @@ use std::collections::HashSet; use object_tree::{Hash, HashedNode}; use petgraph::prelude::*; -use ulid::Ulid; use super::{PkgResult, SiPkgError, Source}; @@ -13,7 +12,7 @@ pub struct SiPkgManagementFunc<'a> { func_unique_id: String, name: String, description: Option, - managed_schemas: Option>, + managed_schemas: Option>, hash: Hash, source: Source<'a>, @@ -58,7 +57,7 @@ impl<'a> SiPkgManagementFunc<'a> { self.description.as_deref() } - pub fn managed_schemas(&self) -> Option<&HashSet> { + pub fn managed_schemas(&self) -> Option<&HashSet> { self.managed_schemas.as_ref() } diff --git a/lib/si-pkg/src/spec/management_func.rs b/lib/si-pkg/src/spec/management_func.rs index 9c6b442806..17b5be5c15 100644 --- a/lib/si-pkg/src/spec/management_func.rs +++ b/lib/si-pkg/src/spec/management_func.rs @@ -2,7 +2,6 @@ use std::collections::HashSet; use derive_builder::Builder; use serde::{Deserialize, Serialize}; -use ulid::Ulid; use super::SpecError; @@ -21,7 +20,7 @@ pub struct ManagementFuncSpec { #[builder(setter(into), default)] #[serde(default)] - pub managed_schemas: Option>, + pub managed_schemas: Option>, } impl ManagementFuncSpec { diff --git a/lib/veritech-client/tests/integration.rs b/lib/veritech-client/tests/integration.rs index 93c0a0416e..99c36ee2c5 100644 --- a/lib/veritech-client/tests/integration.rs +++ b/lib/veritech-client/tests/integration.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{collections::HashMap, env}; use base64::{engine::general_purpose, Engine}; use cyclone_core::{ @@ -106,13 +106,15 @@ async fn executes_simple_management_function() { execution_id: "1234".to_string(), handler: "numberOfInputs".to_string(), this_component: ComponentViewWithGeometry { + kind: None, properties: serde_json::json!({ "foo": "bar", "baz": "quux", "bar": "foo" }), geometry: serde_json::json!({"x": "1", "y": "1"}), }, + components: HashMap::new(), code_base64: base64_encode( "function numberOfInputs({ thisComponent }) { const number = Object.keys(thisComponent.properties)?.length; - return { status: 'ok' message: `${number}` } + return { status: 'ok', message: `${number}` } }", ), before: vec![],