Skip to main content
Glama
func.rs34.2 kB
use std::{ collections::HashMap, string::FromUtf8Error, sync::Arc, }; use argument::{ FuncArgument, FuncArgumentError, }; use authoring::{ FuncAuthoringClient, FuncAuthoringError, }; use base64::{ Engine, engine::general_purpose, }; use binding::{ FuncBinding, FuncBindingError, }; use chrono::{ DateTime, Utc, }; use itertools::Itertools; use serde::{ Deserialize, Serialize, }; use si_events::{ CasValue, ContentHash, Timestamp, ulid::Ulid, }; use si_frontend_types::FuncSummary; use si_pkg::SpecError; use strum::IntoEnumIterator; use telemetry::prelude::*; use thiserror::Error; use ulid::Ulid as CoreUlid; use self::backend::{ FuncBackendKind, FuncBackendResponseType, }; use crate::{ ChangeSetId, DalContext, HelperError, TransactionsError, WsEvent, WsEventResult, WsPayload, change_set::ChangeSetError, func::{ argument::FuncArgumentId, intrinsics::IntrinsicFunc, }, implement_add_edge_to, layer_db_types::FuncContent, pkg, workspace_snapshot::{ WorkspaceSnapshotError, edge_weight::{ EdgeWeightKind, EdgeWeightKindDiscriminants, }, node_weight::{ FuncNodeWeight, NodeWeight, NodeWeightError, category_node_weight::CategoryNodeKind, }, traits::func::FuncExt as _, }, }; pub mod argument; pub mod authoring; pub mod backend; pub mod binding; pub mod debug; pub mod intrinsics; mod kind; pub mod leaf; pub mod runner; pub use kind::FuncKind; #[remain::sorted] #[derive(Error, Debug)] pub enum FuncError { #[error("attribute value error: {0}")] AttributeValue(String), #[error("base64 decode error: {0}")] Base64Decode(#[from] base64::DecodeError), #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), #[error("chrono parse error: {0}")] ChronoParse(#[from] chrono::ParseError), #[error("func argument error: {0}")] FuncArgument(#[from] Box<FuncArgumentError>), #[error("func authoring client error: {0}")] FuncAuthoringClient(#[from] Box<FuncAuthoringError>), #[error("func bindings error: {0}")] FuncBinding(#[from] Box<FuncBindingError>), #[error("func bindings can't be found: {0}")] FuncBindingsLookup(FuncId), #[error("cannot modify locked func: {0}")] FuncLocked(FuncId), #[error("func name already in use {0}")] FuncNameInUse(String), #[error("func to be deleted has bindings: {0}")] FuncToBeDeletedHasBindings(FuncId), #[error("helper error: {0}")] Helper(#[from] HelperError), #[error("cannot find intrinsic func {0}")] IntrinsicFuncNotFound(String), #[error("intrinsic spec creation error: {0}")] IntrinsicSpecCreation(#[source] SpecError), #[error("layer db error: {0}")] LayerDb(#[from] si_layer_cache::LayerDbError), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), #[error("si pkg error: {0}")] Pkg(#[from] Box<pkg::PkgError>), #[error("pkg error: {0}")] SiPkg(#[from] si_pkg::SiPkgError), #[error("pkg spec error: {0}")] Spec(#[from] si_pkg::SpecError), #[error("transactions error: {0}")] Transactions(#[from] TransactionsError), #[error("could not acquire lock: {0}")] TryLock(#[from] tokio::sync::TryLockError), #[error("utf8 error: {0}")] Utf8(#[from] FromUtf8Error), #[error("workspace snapshot error: {0}")] WorkspaceSnapshot(#[from] WorkspaceSnapshotError), } pub type FuncResult<T> = Result<T, FuncError>; impl From<Func> for FuncContent { fn from(value: Func) -> Self { Self::V3(FuncContentV3 { timestamp: value.timestamp, display_name: value.display_name, description: value.description, link: value.link, hidden: value.hidden, builtin: value.builtin, backend_response_type: value.backend_response_type, backend_kind: value.backend_kind, handler: value.handler, code_base64: value.code_base64, code_blake3: value.code_blake3, is_locked: value.is_locked, is_transformation: value.is_transformation, }) } } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct FuncMetadataView { pub display_name: String, pub description: Option<String>, pub link: Option<String>, } pub fn is_intrinsic(name: &str) -> bool { IntrinsicFunc::iter().any(|intrinsic| intrinsic.name() == name) } // NOTE: This is here only for backward compatibility pub use si_id::{ FuncExecutionPk, FuncId, }; use crate::layer_db_types::FuncContentV3; /// A `Func` is the declaration of the existence of a function. It has a name, /// and corresponds to a given function backend (and its associated return types). /// /// `handler` is the name of the entry point into the code in `code_base64`. /// For example, if we had a code block of /// `function myValidator(actual, expected) { return true; }` in `code_base64`, /// the `handler` value should be `myValidator`. #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct Func { pub id: FuncId, pub name: String, pub kind: FuncKind, pub timestamp: Timestamp, pub display_name: Option<String>, pub description: Option<String>, pub link: Option<String>, pub hidden: bool, pub builtin: bool, pub backend_kind: FuncBackendKind, pub backend_response_type: FuncBackendResponseType, pub handler: Option<String>, pub code_base64: Option<String>, pub code_blake3: ContentHash, pub is_locked: bool, pub is_transformation: bool, } impl Func { pub fn assemble(node_weight: &FuncNodeWeight, content: FuncContentV3) -> Self { Self { id: node_weight.id().into(), name: node_weight.name().to_owned(), kind: node_weight.func_kind(), timestamp: content.timestamp, display_name: content.display_name, description: content.description, link: content.link, hidden: content.hidden, builtin: content.builtin, backend_kind: content.backend_kind, backend_response_type: content.backend_response_type, handler: content.handler, code_base64: content.code_base64, code_blake3: content.code_blake3, is_locked: content.is_locked, is_transformation: content.is_transformation, } } implement_add_edge_to!( source_id: FuncId, destination_id: FuncArgumentId, add_fn: add_edge_to_argument, discriminant: EdgeWeightKindDiscriminants::Use, result: FuncResult, ); implement_add_edge_to!( source_id: Ulid, destination_id: FuncId, add_fn: add_category_edge, discriminant: EdgeWeightKindDiscriminants::Use, result: FuncResult, ); #[allow(clippy::too_many_arguments)] pub async fn upsert_with_id( ctx: &DalContext, id: FuncId, name: impl Into<String> + Clone, display_name: Option<impl Into<String>>, description: Option<impl Into<String>>, link: Option<impl Into<String>>, hidden: bool, builtin: bool, backend_kind: FuncBackendKind, backend_response_type: FuncBackendResponseType, handler: Option<impl Into<String>>, code_base64: Option<impl Into<String>>, is_transformation: bool, updated_at: Option<DateTime<Utc>>, ) -> FuncResult<Self> { let timestamp = { let mut timestamp = Timestamp::now(); if let Some(updated) = updated_at { timestamp.updated_at = updated; timestamp.created_at = updated; } timestamp }; let code_base64: Option<String> = code_base64.map(Into::into); let code_blake3 = if let Some(code) = code_base64.as_ref() { let code_json_value: serde_json::Value = code.clone().into(); let code_cas_value: CasValue = code_json_value.into(); let (hash, _) = ctx.layer_db().cas().write( Arc::new(code_cas_value.into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; hash } else { // Why are we doing this? Because the struct gods demand it. I have feelings. ContentHash::new("".as_bytes()) }; let content = FuncContentV3 { timestamp, display_name: display_name.map(Into::into), description: description.map(Into::into), link: link.map(Into::into), hidden, builtin, backend_response_type, backend_kind, handler: handler.map(Into::into), code_base64, code_blake3, is_locked: false, is_transformation, }; let (hash, _) = ctx.layer_db().cas().write( Arc::new(FuncContent::V3(content.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; let func_kind = FuncKind::new(backend_kind, backend_response_type)?; let lineage_id = id.into(); let node_weight = NodeWeight::new_func(id.into(), lineage_id, name.clone().into(), func_kind, hash); let workspace_snapshot = ctx.workspace_snapshot()?; workspace_snapshot .add_or_replace_node(node_weight.clone()) .await?; let func_category_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Func) .await?; Self::add_category_edge(ctx, func_category_id, id, EdgeWeightKind::new_use()).await?; let func_node_weight = node_weight.get_func_node_weight()?; Ok(Self::assemble(&func_node_weight, content)) } #[allow(clippy::too_many_arguments)] pub async fn new( ctx: &DalContext, name: impl Into<String> + Clone, display_name: Option<impl Into<String>>, description: Option<impl Into<String>>, link: Option<impl Into<String>>, hidden: bool, builtin: bool, backend_kind: FuncBackendKind, backend_response_type: FuncBackendResponseType, handler: Option<impl Into<String>>, code_base64: Option<impl Into<String>>, is_transformation: bool, ) -> FuncResult<Self> { let id = ctx.workspace_snapshot()?.generate_ulid().await?.into(); Self::upsert_with_id( ctx, id, name, display_name, description, link, hidden, builtin, backend_kind, backend_response_type, handler, code_base64, is_transformation, None, ) .await } /// Create a debug function with the given code. Debug functions are /// emphemeral, are not stored on the graph, but will be recorded in the /// func run logs when they are executed. pub fn new_debug( name: impl Into<String>, code: impl Into<String>, handler: impl Into<String>, ) -> Self { let id: FuncId = Ulid::new().into(); let name = name.into(); let code = code.into(); let handler = Some(handler.into()); let base64_code = general_purpose::STANDARD_NO_PAD.encode(&code); let code_blake3 = ContentHash::new(code.as_bytes()); Self { id, name, kind: FuncKind::Debug, timestamp: Timestamp::now(), display_name: None, description: None, link: None, hidden: false, builtin: false, backend_kind: FuncBackendKind::Debug, backend_response_type: FuncBackendResponseType::Debug, handler, code_base64: Some(base64_code), code_blake3, is_locked: false, is_transformation: false, } } pub async fn lock(self, ctx: &DalContext) -> FuncResult<Func> { self.modify(ctx, |func| { func.is_locked = true; Ok(()) }) .await } pub fn metadata_view(&self) -> FuncMetadataView { FuncMetadataView { display_name: self .display_name .as_deref() .unwrap_or(self.name.as_str()) .into(), description: self.description.as_deref().map(Into::into), link: None, } } pub async fn get_by_id_opt(ctx: &DalContext, id: FuncId) -> FuncResult<Option<Self>> { let workspace_snapshot = ctx.workspace_snapshot()?; let Some(node_weight) = workspace_snapshot.get_node_weight_opt(id).await else { return Ok(None); }; let func_node_weight = node_weight.get_func_node_weight()?; let hash = func_node_weight.content_hash(); let func = Self::get_by_id_inner(ctx, &hash, &func_node_weight).await?; Ok(Some(func)) } pub async fn get_by_id(ctx: &DalContext, id: FuncId) -> FuncResult<Self> { let func_node_weight = Self::node_weight(ctx, id).await?; Self::get_by_id_inner(ctx, &func_node_weight.content_hash(), &func_node_weight).await } /// If you know the func_id is supposed to be for an [`IntrinsicFunc`], get which one or error pub async fn intrinsic_kind_or_error( ctx: &DalContext, id: FuncId, ) -> FuncResult<IntrinsicFunc> { let func = Self::get_by_id(ctx, id).await?; Self::intrinsic_kind(ctx, id) .await? .ok_or(FuncError::IntrinsicFuncNotFound(func.name)) } pub async fn intrinsic_kind(ctx: &DalContext, id: FuncId) -> FuncResult<Option<IntrinsicFunc>> { let func = Self::get_by_id(ctx, id).await?; Ok(IntrinsicFunc::maybe_from_str(func.name.clone())) } async fn get_by_id_inner( ctx: &DalContext, hash: &ContentHash, func_node_weight: &FuncNodeWeight, ) -> FuncResult<Self> { let content: FuncContent = ctx.layer_db().cas().try_read_as(hash).await?.ok_or( WorkspaceSnapshotError::MissingContentFromStore(func_node_weight.id()), )?; // migrate if necessary! let inner = content.extract(); Ok(Self::assemble(func_node_weight, inner)) } /// Attempt to find the [`FuncId`](Func) by name. /// /// _Warning:_ [`Func`] names are intentionally not unique. This is a greedy algorithm! pub async fn find_id_by_name( ctx: &DalContext, name: impl AsRef<str>, ) -> FuncResult<Option<FuncId>> { let workspace_snapshot = ctx.workspace_snapshot()?; let func_category_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Func) .await?; let func_indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( func_category_id, EdgeWeightKindDiscriminants::Use, ) .await?; let name = name.as_ref(); for func_index in func_indices { let node_weight = workspace_snapshot.get_node_weight(func_index).await?; if let NodeWeight::Func(inner_weight) = node_weight { if inner_weight.name() == name { return Ok(Some(inner_weight.id().into())); } } } Ok(None) } /// Attempt to find the [`FuncId`](Func) by name and [kind](FuncKind). /// /// _Warning:_ [`Func`] names are intentionally not unique. This is a greedy algorithm! pub async fn find_id_by_name_and_kind( ctx: &DalContext, name: impl AsRef<str>, kind: FuncKind, ) -> FuncResult<Option<FuncId>> { let workspace_snapshot = ctx.workspace_snapshot()?; let func_category_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Func) .await?; let func_indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( func_category_id, EdgeWeightKindDiscriminants::Use, ) .await?; let name = name.as_ref(); for func_index in func_indices { let node_weight = workspace_snapshot.get_node_weight(func_index).await?; if let NodeWeight::Func(inner_weight) = node_weight { if inner_weight.name() == name && inner_weight.func_kind() == kind { return Ok(Some(inner_weight.id().into())); } } } Ok(None) } pub fn code_plaintext(&self) -> FuncResult<Option<String>> { Ok(match &self.code_base64 { Some(base64_code) => Some(String::from_utf8( general_purpose::STANDARD_NO_PAD.decode(base64_code)?, )?), None => None, }) } pub async fn is_dynamic(ctx: &DalContext, func_id: FuncId) -> FuncResult<bool> { ctx.workspace_snapshot()?.func_is_dynamic(func_id).await } pub fn is_intrinsic(&self) -> bool { IntrinsicFunc::maybe_from_str(&self.name).is_some() } pub async fn modify_by_id<L>(ctx: &DalContext, id: FuncId, lambda: L) -> FuncResult<Func> where L: FnOnce(&mut Func) -> FuncResult<()>, { let func = Func::get_by_id(ctx, id).await?; let modified_func = func.modify(ctx, lambda).await?; Ok(modified_func) } pub fn error_if_locked(&self) -> FuncResult<()> { if self.is_locked { return Err(FuncError::FuncLocked(self.id)); } Ok(()) } pub async fn node_weight(ctx: &DalContext, func_id: FuncId) -> FuncResult<FuncNodeWeight> { let workspace_snapshot = ctx.workspace_snapshot()?; Ok(workspace_snapshot .get_node_weight(func_id) .await? .get_func_node_weight()?) } /// This _unsafely_ unlocks the [`Func`]. /// /// **Warning:** this should only be used on a case-by-case basis and is dangerous. We should /// create a _copy_ of a [`Func`] when an unlocked one is desired, by default. If unsure, do /// not use this. pub async fn unsafe_unlock_without_copy(self, ctx: &DalContext) -> FuncResult<()> { let mut func = self; let before = FuncContent::from(func.clone()); func.is_locked = false; let updated = FuncContent::from(func.clone()); if updated != before { let (hash, _) = ctx.layer_db().cas().write( Arc::new(updated.into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; ctx.workspace_snapshot()? .update_content(func.id.into(), hash) .await?; } Ok(()) } pub async fn modify<L>(self, ctx: &DalContext, lambda: L) -> FuncResult<Self> where L: FnOnce(&mut Self) -> FuncResult<()>, { let mut func = self; // Variant-level bindings take precedence - if they exist, apply lock check // Only skip lock check if there are NO variant-level bindings (only schema overlays) let has_variant_bindings = FuncBinding::has_variant_bindings(ctx, func.id) .await .map_err(Box::new)?; if has_variant_bindings { func.error_if_locked()?; } let before = FuncContent::from(func.clone()); lambda(&mut func)?; let mut node_weight = Self::node_weight(ctx, func.id).await?; let workspace_snapshot = ctx.workspace_snapshot()?; // If the name HAS changed, *and* parts of the FuncContent // have changed, this ends up updating the node for the function twice. This could be // optimized to do it only once. if func.name.as_str() != node_weight.name() { node_weight.set_name(func.name.as_str()); workspace_snapshot .add_or_replace_node(NodeWeight::Func(node_weight.clone())) .await?; } let updated = FuncContent::from(func.clone()); if updated != before { let (hash, _) = ctx.layer_db().cas().write( Arc::new((updated.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; workspace_snapshot .update_content(func.id.into(), hash) .await?; } Ok(Self::assemble(&node_weight, updated.extract())) } /// Deletes the [`Func`] and returns the name. pub async fn delete_by_id(ctx: &DalContext, id: FuncId) -> FuncResult<String> { let func = Self::get_by_id(ctx, id).await?; // Check that we can remove the func. if !FuncBinding::for_func_id(ctx, id) .await .map_err(Box::new)? .is_empty() { return Err(FuncError::FuncToBeDeletedHasBindings(id)); } // Now, we can remove the func. let workspace_snapshot = ctx.workspace_snapshot()?; workspace_snapshot.remove_node_by_id(id).await?; Ok(func.name) } pub async fn find_intrinsic(ctx: &DalContext, intrinsic: IntrinsicFunc) -> FuncResult<FuncId> { let name = intrinsic.name(); Self::find_id_by_name_and_kind(ctx, name, FuncKind::Intrinsic) .await? .ok_or(FuncError::IntrinsicFuncNotFound(name.to_owned())) } /// List all [`Funcs`](Func) in the workspace pub async fn list_all(ctx: &DalContext) -> FuncResult<Vec<Self>> { let workspace_snapshot = ctx.workspace_snapshot()?; let func_category_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Func) .await?; let func_node_indexes = workspace_snapshot .outgoing_targets_for_edge_weight_kind( func_category_id, EdgeWeightKindDiscriminants::Use, ) .await?; let mut func_node_weights = Vec::with_capacity(func_node_indexes.len()); let mut func_content_hashes = Vec::with_capacity(func_node_indexes.len()); for index in func_node_indexes { let node_weight = workspace_snapshot .get_node_weight(index) .await? .get_func_node_weight()?; func_content_hashes.push(node_weight.content_hash()); func_node_weights.push(node_weight); } Self::list_inner(ctx, func_node_weights, func_content_hashes).await } /// List all [`Funcs`](Func) in the workspace that are either unlocked, attached to a default /// [`SchemaVariant`] or attached to an unlocked Schema Variant pub async fn list_for_default_and_editing(ctx: &DalContext) -> FuncResult<Vec<Self>> { let funcs = Self::list_all(ctx).await?; let mut pruned_funcs = vec![]; for func in funcs { if func.is_locked { match FuncBinding::get_bindings_for_default_schema_variants(ctx, func.id).await { Ok(b) => { if !b.is_empty() { pruned_funcs.push(func); } else { let bindings = FuncBinding::for_func_id(ctx, func.id) .await .map_err(|_| FuncError::FuncBindingsLookup(func.id))?; if bindings.is_empty() { pruned_funcs.push(func) } } } Err(err) => { error!(?err, "could not get bindings for func id {}", func.id) } } } else { pruned_funcs.push(func); } } Ok(pruned_funcs) } /// List all [`Funcs`](Func) corresponding to the provided [`FuncIds`](Func). pub async fn list_from_ids(ctx: &DalContext, func_ids: &[FuncId]) -> FuncResult<Vec<Self>> { let workspace_snapshot = ctx.workspace_snapshot()?; let mut func_node_weights = Vec::with_capacity(func_ids.len()); let mut func_content_hashes = Vec::with_capacity(func_ids.len()); for id in func_ids { let node_weight = workspace_snapshot .get_node_weight(id) .await? .get_func_node_weight()?; func_content_hashes.push(node_weight.content_hash()); func_node_weights.push(node_weight); } Self::list_inner(ctx, func_node_weights, func_content_hashes).await } async fn list_inner( ctx: &DalContext, func_node_weights: Vec<FuncNodeWeight>, func_content_hashes: Vec<ContentHash>, ) -> FuncResult<Vec<Self>> { let func_contents: HashMap<ContentHash, FuncContent> = ctx .layer_db() .cas() .try_read_many_as(func_content_hashes.as_slice()) .await?; let mut funcs = Vec::with_capacity(func_node_weights.len()); for node_weight in func_node_weights { match func_contents.get(&node_weight.content_hash()) { Some(func_content) => { // migrates if needed! let content = func_content.clone().extract(); funcs.push(Func::assemble(&node_weight, content)); } None => Err(WorkspaceSnapshotError::MissingContentFromStore( node_weight.id(), ))?, } } Ok(funcs) } /// Creates an exact clone of the current func that is not locked, including recreating all /// [`FuncArgument`]s pub async fn create_unlocked_func_copy(&self, ctx: &DalContext) -> FuncResult<Self> { let new_func = Self::new( ctx, self.name.clone(), self.display_name.clone(), self.description.clone(), self.link.clone(), self.hidden, false, self.backend_kind, self.backend_response_type, self.handler.clone(), self.code_base64.clone(), self.is_transformation, ) .await?; for arg in FuncArgument::list_for_func(ctx, self.id) .await .map_err(Box::new)? { // create new func args for the new func FuncArgument::new(ctx, arg.name, arg.kind, arg.element_kind, new_func.id) .await .map_err(Box::new)?; } Ok(new_func) } pub async fn clone_func_with_new_name( &self, ctx: &DalContext, new_name: String, ) -> FuncResult<Self> { if new_name == self.name.clone() { return Err(FuncError::FuncNameInUse(new_name)); } let duplicated_func = Self::new( ctx, new_name, self.display_name.clone(), self.description.clone(), self.link.clone(), self.hidden, false, self.backend_kind, self.backend_response_type, self.handler.clone(), self.code_base64.clone(), self.is_transformation, ) .await?; Ok(duplicated_func) } pub async fn into_frontend_type(&self, ctx: &DalContext) -> FuncResult<FuncSummary> { let bindings: Vec<FuncBinding> = FuncBinding::for_func_id(ctx, self.id) .await .map_err(Box::new)?; self.into_frontend_type_sideload_bindings(ctx, bindings) .await } pub async fn into_frontend_type_sideload_bindings( &self, ctx: &DalContext, bindings: Vec<FuncBinding>, ) -> FuncResult<FuncSummary> { let bindings: Vec<si_frontend_types::FuncBinding> = bindings.into_iter().map(Into::into).collect_vec(); 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), name: arg.name.clone(), kind: arg.kind.into(), element_kind: arg.element_kind.map(Into::into), timestamp: arg.timestamp, }); } let types = self.get_types(ctx).await?; Ok(FuncSummary { func_id: self.id, kind: self.kind.into(), name: self.name.clone(), backend_kind: self.backend_kind.into(), display_name: self.display_name.clone(), description: self.description.clone(), is_locked: self.is_locked, bindings, arguments, types: Some(types), is_transformation: self.is_transformation, }) } // helper to get updated types to fire WSEvents so SDF can decide when these events need to fire pub async fn get_types(&self, ctx: &DalContext) -> FuncResult<String> { let types = [ FuncAuthoringClient::compile_return_types( self.backend_response_type, self.backend_kind, ), FuncAuthoringClient::compile_types_from_bindings(ctx, self.id) .await .map_err(Box::new)? .as_str(), FuncAuthoringClient::compile_langjs_types(), ] .join("\n"); Ok(types) } /// Get a short, human-readable title suitable for debugging/display. pub async fn fmt_title(ctx: &DalContext, id: FuncId) -> String { Self::fmt_title_fallible(ctx, id) .await .unwrap_or_else(|e| e.to_string()) } async fn fmt_title_fallible(ctx: &DalContext, id: FuncId) -> FuncResult<String> { Ok(Self::node_weight(ctx, id).await?.name) } } #[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, client_ulid: Option<CoreUlid>, } #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FuncWsEventCodeSaved { change_set_id: ChangeSetId, func_code: si_frontend_types::FuncCode, generated: bool, } #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FuncWsEventGeneratingAwsCliCommand { command: String, subcommand: String, } #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FuncWsEventGenerating { func_id: FuncId, command: FuncWsEventGeneratingAwsCliCommand, } impl WsEvent { pub async fn func_arguments_saved(ctx: &DalContext, func_id: FuncId) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncArgumentsSaved(FuncWsEventPayload { func_id, change_set_id: ctx.change_set_id(), }), ) .await } pub async fn func_deleted(ctx: &DalContext, func_id: FuncId) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncDeleted(FuncWsEventPayload { func_id, change_set_id: ctx.change_set_id(), }), ) .await } pub async fn func_saved(ctx: &DalContext, func_id: FuncId) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncSaved(FuncWsEventPayload { func_id, change_set_id: ctx.change_set_id(), }), ) .await } pub async fn func_updated( ctx: &DalContext, func_summary: si_frontend_types::FuncSummary, client_ulid: Option<CoreUlid>, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncUpdated(FuncWsEventFuncSummary { change_set_id: ctx.change_set_id(), func_summary, client_ulid, }), ) .await } pub async fn func_created( ctx: &DalContext, func_summary: si_frontend_types::FuncSummary, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncCreated(FuncWsEventFuncSummary { change_set_id: ctx.change_set_id(), func_summary, client_ulid: None, }), ) .await } pub async fn func_code_saved( ctx: &DalContext, func_code: si_frontend_types::FuncCode, generated: bool, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncCodeSaved(FuncWsEventCodeSaved { change_set_id: ctx.change_set_id(), func_code, generated, }), ) .await } pub async fn func_generating( ctx: &DalContext, func_id: FuncId, command: String, subcommand: String, ) -> WsEventResult<Self> { WsEvent::new( ctx, WsPayload::FuncGenerating(FuncWsEventGenerating { func_id, command: FuncWsEventGeneratingAwsCliCommand { command, subcommand, }, }), ) .await } }

Latest Blog Posts

MCP directory API

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

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

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