diff --git a/app/web/src/components/AssetDetailsPanel.vue b/app/web/src/components/AssetDetailsPanel.vue index 7790a021fd..617ee254a9 100644 --- a/app/web/src/components/AssetDetailsPanel.vue +++ b/app/web/src/components/AssetDetailsPanel.vue @@ -9,47 +9,56 @@
+ +
{{ warning.message }} @@ -64,59 +73,59 @@ - + @@ -146,10 +155,10 @@ {{ @@ -177,6 +186,7 @@ import { FuncKind } from "@/api/sdf/dal/func"; import { useAssetStore } from "@/store/asset.store"; import { FuncId } from "@/store/func/funcs.store"; import { ComponentType } from "@/api/sdf/dal/diagram"; +import { useFeatureFlagsStore } from "@/store/feature_flags.store"; import ColorPicker from "./ColorPicker.vue"; import AssetFuncAttachModal from "./AssetFuncAttachModal.vue"; import AssetNameModal from "./AssetNameModal.vue"; @@ -244,6 +254,14 @@ const executeAsset = async () => { } }; +const unlock = async () => { + if (assetStore.selectedAsset?.defaultSchemaVariantId) { + await assetStore.CREATE_UNLOCKED_COPY( + assetStore.selectedAsset?.defaultSchemaVariantId, + ); + } +}; + const closeHandler = () => { assetStore.executeAssetTaskId = undefined; }; diff --git a/app/web/src/store/asset.store.ts b/app/web/src/store/asset.store.ts index 4191783a28..efdc1e32ed 100644 --- a/app/web/src/store/asset.store.ts +++ b/app/web/src/store/asset.store.ts @@ -11,6 +11,7 @@ import router from "@/router"; import { PropKind } from "@/api/sdf/dal/prop"; import { ComponentType } from "@/api/sdf/dal/diagram"; import { nonNullable } from "@/utils/typescriptLinter"; +import { SchemaVariantId } from "@/api/sdf/dal/schema"; import { useChangeSetsStore } from "./change_sets.store"; import { useRealtimeStore } from "./realtime/realtime.store"; import { @@ -464,6 +465,25 @@ export const useAssetStore = () => { }, }); }, + + async CREATE_UNLOCKED_COPY(id: SchemaVariantId) { + if (changeSetsStore.creatingChangeSet) + throw new Error("race, wait until the change set is created"); + if (changeSetsStore.headSelected) + changeSetsStore.creatingChangeSet = true; + + this.detachmentWarnings = []; + + return new ApiRequest({ + method: "post", + url: "/variant/create_unlocked_copy", + keyRequestStatusBy: id, + params: { + ...visibility, + id, + }, + }); + }, }, async onActivated() { await this.LOAD_ASSET_LIST(); diff --git a/app/web/src/store/feature_flags.store.ts b/app/web/src/store/feature_flags.store.ts index 54ff68f09e..e2565d9d52 100644 --- a/app/web/src/store/feature_flags.store.ts +++ b/app/web/src/store/feature_flags.store.ts @@ -7,6 +7,7 @@ import { posthog } from "@/utils/posthog"; const FLAG_MAPPING = { // STORE_FLAG_NAME: "posthogFlagName", MODULES_TAB: "modules_tab", + IMMUTABLE_SCHEMA_VARIANTS: "immutable_schema_variants", }; type FeatureFlags = keyof typeof FLAG_MAPPING; diff --git a/lib/dal/src/attribute/prototype.rs b/lib/dal/src/attribute/prototype.rs index cfd25ebeb9..be10473850 100644 --- a/lib/dal/src/attribute/prototype.rs +++ b/lib/dal/src/attribute/prototype.rs @@ -657,6 +657,27 @@ impl AttributePrototype { } Ok(sources) } + + pub async fn list_arguments_for_id( + ctx: &DalContext, + ap_id: AttributePrototypeId, + ) -> AttributePrototypeResult> { + let mut apas = vec![]; + + let snap = ctx.workspace_snapshot()?; + + for idx in snap + .outgoing_targets_for_edge_weight_kind( + ap_id, + EdgeWeightKindDiscriminants::PrototypeArgument, + ) + .await? + { + apas.push(snap.get_node_weight(idx).await?.id().into()) + } + + Ok(apas) + } } #[remain::sorted] diff --git a/lib/dal/src/attribute/prototype/argument.rs b/lib/dal/src/attribute/prototype/argument.rs index 3a37cc4df2..0c1e5ea9ec 100644 --- a/lib/dal/src/attribute/prototype/argument.rs +++ b/lib/dal/src/attribute/prototype/argument.rs @@ -74,6 +74,8 @@ pub enum AttributePrototypeArgumentError { NodeWeight(#[from] NodeWeightError), #[error("no targets for prototype argument: {0}")] NoTargets(AttributePrototypeArgumentId), + #[error("prototype argument not found for attribute prototype {0} and func arg {1}")] + NotFoundForApAndFuncArg(AttributePrototypeId, FuncArgumentId), #[error("serde json error: {0}")] Serde(#[from] serde_json::Error), #[error("transactions error: {0}")] @@ -322,6 +324,27 @@ impl AttributePrototypeArgument { Err(AttributePrototypeArgumentError::MissingFuncArgument(apa_id)) } + pub async fn find_by_func_argument_id_and_attribute_prototype_id( + ctx: &DalContext, + func_argument_id: FuncArgumentId, + ap_id: AttributePrototypeId, + ) -> AttributePrototypeArgumentResult { + // AP --> APA --> Func Arg + + for apa_id in AttributePrototype::list_arguments_for_id(ctx, ap_id).await? { + let this_func_arg_id = Self::func_argument_id_by_id(ctx, apa_id).await?; + + if this_func_arg_id == func_argument_id { + return Ok(apa_id); + } + } + + Err(AttributePrototypeArgumentError::NotFoundForApAndFuncArg( + ap_id, + func_argument_id, + )) + } + pub async fn value_source( &self, ctx: &DalContext, diff --git a/lib/dal/src/diagram.rs b/lib/dal/src/diagram.rs index aaefbaeb2a..7bd8b46f17 100644 --- a/lib/dal/src/diagram.rs +++ b/lib/dal/src/diagram.rs @@ -227,7 +227,7 @@ impl SummaryDiagramComponent { schema_name: schema.name().to_owned(), schema_id: schema.id(), schema_variant_id: schema_variant.id(), - schema_variant_name: schema_variant.name().to_owned(), + schema_variant_name: schema_variant.version().to_owned(), schema_category: schema_variant.category().to_owned(), display_name: component.name(ctx).await?, position, diff --git a/lib/dal/src/history_event.rs b/lib/dal/src/history_event.rs index f979f57090..fcc520a0dc 100644 --- a/lib/dal/src/history_event.rs +++ b/lib/dal/src/history_event.rs @@ -43,6 +43,7 @@ impl HistoryActor { HistoryActor::SystemInit => "unknown-backend".to_string(), } } + pub async fn email(&self, ctx: &DalContext) -> HistoryEventResult { Ok(match self { HistoryActor::SystemInit => "sally@systeminit.com".to_string(), diff --git a/lib/dal/src/layer_db_types/content_types.rs b/lib/dal/src/layer_db_types/content_types.rs index d7ed2793d8..6adaf3c07a 100644 --- a/lib/dal/src/layer_db_types/content_types.rs +++ b/lib/dal/src/layer_db_types/content_types.rs @@ -2,17 +2,28 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use si_events::{CasValue, ContentHash}; use strum::EnumDiscriminants; +use thiserror::Error; use crate::action::prototype::ActionKind; use crate::validation::ValidationStatus; use crate::{ action::ActionCompletionStatus, func::argument::FuncArgumentKind, prop::WidgetOptions, property_editor::schema::WidgetKind, socket::connection_annotation::ConnectionAnnotation, - ActionPrototypeId, ComponentId, ComponentType, FuncBackendKind, FuncBackendResponseType, - FuncId, PropId, PropKind, SocketArity, SocketKind, Timestamp, UserPk, + ActionPrototypeId, ComponentId, ComponentType, DalContext, FuncBackendKind, + FuncBackendResponseType, FuncId, PropId, PropKind, SchemaVariant, SchemaVariantId, SocketArity, + SocketKind, Timestamp, UserPk, }; -/// This type gathers up all the kinds of things we will store in the +#[remain::sorted] +#[derive(Error, Debug)] +pub enum ContentTypeError { + #[error("error extracting schema variant content : {0}")] + SchemaVariantContent(String), +} + +pub type ContentTypeResult = Result; + +/// This type gathers all the kinds of things we will store in the /// content-store portion of the layered database. Anything we want to read or /// write from there should be added here. Then the impl_into_content_types! /// macro should be used to provide from/into implementations between the inner @@ -346,6 +357,54 @@ pub struct SchemaContentV1 { #[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] pub enum SchemaVariantContent { V1(SchemaVariantContentV1), + V2(SchemaVariantContentV2), +} + +impl SchemaVariantContent { + pub async fn extract( + self, + ctx: &DalContext, + id: SchemaVariantId, + ) -> ContentTypeResult { + // update progressively + let at_least_v2 = match self { + SchemaVariantContent::V1(v1) => { + let display_name = if let Some(display_name) = v1.display_name { + display_name + } else { + let schema = SchemaVariant::schema_for_schema_variant_id(ctx, id) + .await + .map_err(|e| ContentTypeError::SchemaVariantContent(e.to_string()))?; + schema.name + }; + + SchemaVariantContent::V2(SchemaVariantContentV2 { + timestamp: v1.timestamp, + ui_hidden: v1.ui_hidden, + version: v1.name.to_owned(), + display_name, + category: v1.category, + color: v1.color, + component_type: v1.component_type, + link: v1.link, + description: v1.description, + asset_func_id: v1.asset_func_id, + finalized_once: v1.finalized_once, + is_builtin: v1.is_builtin, + is_locked: true, + }) + } + later => later, + }; + + // extract latest + let latest = match at_least_v2 { + SchemaVariantContent::V2(v2) => v2, + _ => unreachable!(), + }; + + Ok(latest) + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -364,6 +423,23 @@ pub struct SchemaVariantContentV1 { pub is_builtin: bool, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct SchemaVariantContentV2 { + pub timestamp: Timestamp, + pub ui_hidden: bool, + pub version: String, + pub display_name: String, + pub category: String, + pub color: String, + pub component_type: ComponentType, + pub link: Option, + pub description: Option, + pub asset_func_id: Option, + pub finalized_once: bool, + pub is_builtin: bool, + pub is_locked: bool, +} + #[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] pub enum SecretContent { V1(SecretContentV1), diff --git a/lib/dal/src/pkg/export.rs b/lib/dal/src/pkg/export.rs index cc7b5b378d..754d1e17ca 100644 --- a/lib/dal/src/pkg/export.rs +++ b/lib/dal/src/pkg/export.rs @@ -170,12 +170,12 @@ impl PkgExporter { variant_is_builtin: bool, ) -> PkgResult { let mut variant_spec_builder = SchemaVariantSpec::builder(); - variant_spec_builder.name(variant.name()); + variant_spec_builder.name(variant.version()); variant_spec_builder.is_builtin(variant_is_builtin); variant_spec_builder.unique_id(variant.id().to_string()); let mut data_builder = SchemaVariantSpecData::builder(); - data_builder.name(variant.name()); + data_builder.name(variant.version()); data_builder.color(variant.get_color(ctx).await?); if let Some(link) = variant.link() { diff --git a/lib/dal/src/pkg/import.rs b/lib/dal/src/pkg/import.rs index 825c3b752f..92337e881a 100644 --- a/lib/dal/src/pkg/import.rs +++ b/lib/dal/src/pkg/import.rs @@ -468,7 +468,7 @@ async fn import_schema( for variant_spec in &schema_spec.variants()? { let variant = import_schema_variant( ctx, - &mut schema, + &schema, schema_spec.clone(), variant_spec, installed_module.clone(), @@ -478,7 +478,10 @@ async fn import_schema( .await?; if let Some(variant) = variant { - installed_schema_variant_ids.push(variant.id()); + let variant_id = variant.id(); + variant.lock(ctx).await?; + + installed_schema_variant_ids.push(variant_id); set_default_schema_variant_id( ctx, @@ -488,7 +491,7 @@ async fn import_schema( .as_ref() .and_then(|data| data.default_schema_variant()), variant_spec.unique_id(), - variant.id(), + variant_id, ) .await?; } @@ -943,7 +946,7 @@ pub async fn import_only_new_funcs( #[allow(clippy::too_many_arguments)] pub(crate) async fn import_schema_variant( ctx: &DalContext, - schema: &mut Schema, + schema: &Schema, schema_spec: SiPkgSchema<'_>, variant_spec: &SiPkgSchemaVariant<'_>, installed_module: Option, @@ -957,7 +960,7 @@ pub(crate) async fn import_schema_variant( installed_pkg.list_associated_schema_variants(ctx).await?; let mut maybe_matching_schema_variant: Vec = associated_schema_variants .into_iter() - .filter(|s| s.name() == schema_spec.name()) + .filter(|s| s.version() == schema_spec.name()) .collect(); if let Some(matching_schema_variant) = maybe_matching_schema_variant.pop() { existing_schema_variant = Some(matching_schema_variant); @@ -968,10 +971,11 @@ pub(crate) async fn import_schema_variant( existing_schema_variant = Some(existing_sv); } + // TODO `created` is always true. if that's correct, none of the sv vars should be optional, including the return type let (variant, created) = match existing_schema_variant { None => { let spec = schema_spec.to_spec().await?; - let schema_name = &spec.name.clone(); + let schema_name = spec.name.clone(); let metadata = SchemaVariantJson::metadata_from_spec(spec)?; let mut asset_func_id: Option = None; @@ -988,29 +992,24 @@ pub(crate) async fn import_schema_variant( asset_func_id = Some(asset_func.id) } } - let display_name = match metadata.display_name { - Some(name) => name, - None => schema_name.into(), - }; - info!(%display_name, "display_name"); - ( - SchemaVariant::new( - ctx, - schema.id(), - variant_spec.name(), - display_name, - metadata.category, - metadata.color, - metadata.component_type, - metadata.link, - metadata.description, - asset_func_id, - variant_spec.is_builtin(), - ) - .await? - .0, - true, + + let variant = SchemaVariant::new( + ctx, + schema.id(), + variant_spec.name(), + metadata.display_name.unwrap_or(schema_name), + metadata.category, + metadata.color, + metadata.component_type, + metadata.link, + metadata.description, + asset_func_id, + variant_spec.is_builtin(), ) + .await? + .0; + + (variant, true) } Some(installed_variant) => (installed_variant, true), }; diff --git a/lib/dal/src/prop.rs b/lib/dal/src/prop.rs index d1599eb01d..713afbcb7c 100644 --- a/lib/dal/src/prop.rs +++ b/lib/dal/src/prop.rs @@ -625,7 +625,7 @@ impl Prop { let mut work_queue = VecDeque::from([prop_id]); while let Some(prop_id) = work_queue.pop_front() { - if let Some(prop_id) = Prop::parent_prop_id_by_id(ctx, prop_id).await? { + if let Some(prop_id) = Self::parent_prop_id_by_id(ctx, prop_id).await? { let workspace_snapshot = ctx.workspace_snapshot()?; let node_idx = workspace_snapshot.get_node_index_by_id(prop_id).await?; @@ -690,7 +690,7 @@ impl Prop { // NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here. let PropContent::V1(inner) = content; - Ok(Prop::assemble(node_weight, inner)) + Ok(Self::assemble(node_weight, inner)) } pub async fn element_prop_id(&self, ctx: &DalContext) -> PropResult { @@ -917,7 +917,7 @@ impl Prop { ctx: &DalContext, prop_id: PropId, ) -> PropResult> { - let prototype_id = Prop::prototype_id(ctx, prop_id).await?; + let prototype_id = Self::prototype_id(ctx, prop_id).await?; let prototype_func = Func::get_by_id_or_error(ctx, AttributePrototype::func_id(ctx, prototype_id).await?) .await?; @@ -951,12 +951,12 @@ impl Prop { ) -> PropResult<()> { let value = serde_json::to_value(value)?; - let prop = Prop::get_by_id_or_error(ctx, prop_id).await?; + let prop = Self::get_by_id_or_error(ctx, prop_id).await?; if !prop.kind.is_scalar() { return Err(PropError::SetDefaultForNonScalar(prop_id, prop.kind)); } - let prototype_id = Prop::prototype_id(ctx, prop_id).await?; + let prototype_id = Self::prototype_id(ctx, prop_id).await?; let intrinsic: IntrinsicFunc = prop.kind.into(); let intrinsic_id = Func::find_intrinsic(ctx, intrinsic).await?; let func_arg_id = *FuncArgument::list_ids_for_func(ctx, intrinsic_id) @@ -1082,6 +1082,16 @@ impl Prop { Ok(ordered_child_props) } + pub async fn find_equivalent_in_schema_variant( + ctx: &DalContext, + prop_id: PropId, + schema_variant_id: SchemaVariantId, + ) -> PropResult { + let prop_path = Self::path_by_id(ctx, prop_id).await?; + + 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 { diff --git a/lib/dal/src/schema/variant.rs b/lib/dal/src/schema/variant.rs index 11d1454056..b85b68a3fd 100644 --- a/lib/dal/src/schema/variant.rs +++ b/lib/dal/src/schema/variant.rs @@ -21,8 +21,8 @@ use crate::func::argument::{FuncArgument, FuncArgumentError}; use crate::func::intrinsics::IntrinsicFunc; use crate::func::{FuncError, FuncKind}; use crate::layer_db_types::{ - FuncContent, InputSocketContent, OutputSocketContent, SchemaVariantContent, - SchemaVariantContentV1, + ContentTypeError, FuncContent, InputSocketContent, OutputSocketContent, SchemaVariantContent, + SchemaVariantContentV2, }; use crate::prop::{PropError, PropPath}; use crate::schema::variant::root_prop::RootProp; @@ -84,6 +84,8 @@ pub enum SchemaVariantError { ChangeSet(#[from] ChangeSetError), #[error("component error: {0}")] Component(#[from] Box), + #[error("content error: {0}")] + ContentType(#[from] ContentTypeError), #[error("default schema variant not found for schema: {0}")] DefaultSchemaVariantNotFound(SchemaId), #[error("default variant not found: {0}")] @@ -158,8 +160,8 @@ pub struct SchemaVariant { #[serde(flatten)] timestamp: Timestamp, ui_hidden: bool, - name: String, - display_name: Option, + version: String, + display_name: String, category: String, color: String, component_type: ComponentType, @@ -168,6 +170,7 @@ pub struct SchemaVariant { asset_func_id: Option, finalized_once: bool, is_builtin: bool, + is_locked: bool, } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] @@ -296,12 +299,12 @@ impl WsEvent { } } -impl From for SchemaVariantContentV1 { +impl From for SchemaVariantContent { fn from(value: SchemaVariant) -> Self { - Self { + Self::V2(SchemaVariantContentV2 { timestamp: value.timestamp, ui_hidden: value.ui_hidden, - name: value.name, + version: value.version, display_name: value.display_name, category: value.category, color: value.color, @@ -311,16 +314,23 @@ impl From for SchemaVariantContentV1 { asset_func_id: value.asset_func_id, finalized_once: value.finalized_once, is_builtin: value.is_builtin, - } + is_locked: value.is_locked, + }) } } impl SchemaVariant { - pub fn assemble(id: SchemaVariantId, inner: SchemaVariantContentV1) -> Self { - Self { + pub async fn assemble( + ctx: &DalContext, + id: SchemaVariantId, + content: SchemaVariantContent, + ) -> SchemaVariantResult { + let inner = content.extract(ctx, id).await?; + + Ok(Self { id, timestamp: inner.timestamp, - name: inner.name, + version: inner.version, display_name: inner.display_name, category: inner.category, color: inner.color, @@ -331,15 +341,16 @@ impl SchemaVariant { ui_hidden: inner.ui_hidden, finalized_once: inner.finalized_once, is_builtin: inner.is_builtin, - } + is_locked: inner.is_locked, + }) } #[allow(clippy::too_many_arguments)] pub async fn new( ctx: &DalContext, schema_id: SchemaId, - name: impl Into, - display_name: impl Into>, + version: impl Into, + display_name: impl Into, category: impl Into, color: impl Into, component_type: impl Into, @@ -351,9 +362,9 @@ impl SchemaVariant { debug!(%schema_id, "creating schema variant and root prop tree"); let workspace_snapshot = ctx.workspace_snapshot()?; - let content = SchemaVariantContentV1 { + let content = SchemaVariantContentV2 { timestamp: Timestamp::now(), - name: name.into(), + version: version.into(), link: link.into(), ui_hidden: false, finalized_once: false, @@ -364,13 +375,14 @@ impl SchemaVariant { description: description.into(), asset_func_id, is_builtin, + is_locked: false, }; let (hash, _) = ctx .layer_db() .cas() .write( - Arc::new(SchemaVariantContent::V1(content.clone()).into()), + Arc::new(SchemaVariantContent::V2(content.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), @@ -390,7 +402,8 @@ impl SchemaVariant { let root_prop = RootProp::new(ctx, schema_variant_id).await?; let _func_id = Func::find_intrinsic(ctx, IntrinsicFunc::Identity).await?; - let schema_variant = Self::assemble(id.into(), content); + let schema_variant = + Self::assemble(ctx, id.into(), SchemaVariantContent::V2(content)).await?; Ok((schema_variant, root_prop)) } @@ -400,16 +413,16 @@ impl SchemaVariant { { let mut schema_variant = self; - let before = SchemaVariantContentV1::from(schema_variant.clone()); + let before = SchemaVariantContent::from(schema_variant.clone()); lambda(&mut schema_variant)?; - let updated = SchemaVariantContentV1::from(schema_variant.clone()); + let updated = SchemaVariantContent::from(schema_variant.clone()); if updated != before { let (hash, _) = ctx .layer_db() .cas() .write( - Arc::new(SchemaVariantContent::V1(updated.clone()).into()), + Arc::new(updated.into()), None, ctx.events_tenancy(), ctx.events_actor(), @@ -424,6 +437,14 @@ impl SchemaVariant { Ok(schema_variant) } + pub async fn lock(self, ctx: &DalContext) -> SchemaVariantResult { + self.modify(ctx, |sv| { + sv.is_locked = true; + Ok(()) + }) + .await + } + /// Returns all [`PropIds`](Prop) for a given [`SchemaVariantId`](SchemaVariant). pub async fn all_prop_ids( ctx: &DalContext, @@ -494,10 +515,7 @@ impl SchemaVariant { .await? .ok_or(WorkspaceSnapshotError::MissingContentFromStore(id.into()))?; - // NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here - let SchemaVariantContent::V1(inner) = content; - - Ok(Self::assemble(id, inner)) + Self::assemble(ctx, id, content).await } /// Lists all [`Components`](Component) that are using the provided [`SchemaVariantId`](SchemaVariant). @@ -631,10 +649,8 @@ impl SchemaVariant { for node_weight in node_weights { match content_map.get(&node_weight.content_hash()) { Some(content) => { - // NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here. - let SchemaVariantContent::V1(inner) = content; - - schema_variants.push(Self::assemble(node_weight.id().into(), inner.to_owned())); + schema_variants + .push(Self::assemble(ctx, node_weight.id().into(), content.clone()).await?); } None => Err(WorkspaceSnapshotError::MissingContentFromStore( node_weight.id(), @@ -653,20 +669,25 @@ impl SchemaVariant { self.ui_hidden } - pub fn name(&self) -> &str { - &self.name + pub fn version(&self) -> &str { + &self.version } pub fn category(&self) -> &str { &self.category } + pub fn color(&self) -> &str { + &self.color + } + pub fn timestamp(&self) -> Timestamp { self.timestamp } + // TODO update this to not be optional - and update frontend pub fn display_name(&self) -> Option { - self.display_name.clone() + Some(self.display_name.clone()) } pub fn link(&self) -> Option { @@ -684,10 +705,15 @@ impl SchemaVariant { pub fn asset_func_id(&self) -> Option { self.asset_func_id } + pub fn is_builtin(&self) -> bool { self.is_builtin } + pub fn is_locked(&self) -> bool { + self.is_locked + } + pub async fn get_root_prop_id( ctx: &DalContext, schema_variant_id: SchemaVariantId, @@ -941,7 +967,7 @@ impl SchemaVariant { async fn get_content( ctx: &DalContext, schema_variant_id: SchemaVariantId, - ) -> SchemaVariantResult<(ContentHash, SchemaVariantContentV1)> { + ) -> SchemaVariantResult<(ContentHash, SchemaVariantContentV2)> { let workspace_snapshot = ctx.workspace_snapshot()?; let id: Ulid = schema_variant_id.into(); let node_index = workspace_snapshot.get_node_index_by_id(id).await?; @@ -955,10 +981,7 @@ impl SchemaVariant { .await? .ok_or(WorkspaceSnapshotError::MissingContentFromStore(id))?; - // NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here. - let SchemaVariantContent::V1(inner) = content; - - Ok((hash, inner)) + Ok((hash, content.extract(ctx, schema_variant_id).await?)) } /// This _idempotent_ function "finalizes" a [`SchemaVariant`]. diff --git a/lib/dal/src/schema/variant/authoring.rs b/lib/dal/src/schema/variant/authoring.rs index 8e49a2d634..2115534639 100644 --- a/lib/dal/src/schema/variant/authoring.rs +++ b/lib/dal/src/schema/variant/authoring.rs @@ -13,12 +13,19 @@ use si_pkg::{ use telemetry::prelude::*; use thiserror::Error; +use crate::action::prototype::ActionPrototypeError; +use crate::attribute::prototype::argument::AttributePrototypeArgumentError; +use crate::attribute::prototype::AttributePrototypeError; +use crate::func::authoring::FuncAuthoringError; use crate::func::intrinsics::IntrinsicFunc; use crate::func::runner::{FuncRunner, FuncRunnerError}; use crate::pkg::export::PkgExporter; use crate::pkg::import::import_only_new_funcs; use crate::pkg::{import_pkg_from_pkg, PkgError}; +use crate::prop::PropError; use crate::schema::variant::{SchemaVariantJson, SchemaVariantMetadataJson}; +use crate::socket::input::InputSocketError; +use crate::socket::output::OutputSocketError; use crate::{ pkg, ComponentType, DalContext, Func, FuncBackendKind, FuncBackendResponseType, FuncError, FuncId, HistoryEventError, Schema, SchemaError, SchemaVariant, SchemaVariantError, @@ -29,8 +36,16 @@ use crate::{ #[remain::sorted] #[derive(Error, Debug)] pub enum VariantAuthoringError { + #[error("action prototype error: {0}")] + ActionPrototype(#[from] ActionPrototypeError), + #[error("attribute prototype error: {0}")] + AttributePrototype(#[from] AttributePrototypeError), + #[error("attribute prototype error: {0}")] + AttributePrototypeArgument(#[from] AttributePrototypeArgumentError), #[error("func error: {0}")] Func(#[from] FuncError), + #[error("func authoring error: {0}")] + FuncAuthoring(#[from] FuncAuthoringError), #[error("func execution error: {0}")] FuncExecution(FuncId), #[error("func execution failure error: {0}")] @@ -41,10 +56,14 @@ pub enum VariantAuthoringError { FuncRunGone, #[error("history event error: {0}")] HistoryEvent(#[from] HistoryEventError), + #[error("input socket error: {0}")] + InputSocket(#[from] InputSocketError), #[error("layer db error: {0}")] LayerDb(#[from] LayerDbError), #[error("no new asset was created")] NoAssetCreated, + #[error("output socket error: {0}")] + OutputSocket(#[from] OutputSocketError), #[error("pkg error: {0}")] Pkg(#[from] PkgError), #[error("constructed package has no identity function")] @@ -53,6 +72,8 @@ pub enum VariantAuthoringError { PkgMissingSchema, #[error("constructed package has no schema variant node")] PkgMissingSchemaVariant, + #[error("prop error: {0}")] + Prop(#[from] PropError), #[error("schema error: {0}")] Schema(#[from] SchemaError), #[error("schema variant error: {0}")] @@ -153,12 +174,12 @@ impl VariantAuthoringClient { Ok(SchemaVariant::get_by_id(ctx, schema_variant_id).await?) } + // TODO RENAME THIS, it clones a schema with a variant #[instrument(name = "variant.authoring.clone_variant", level = "info", skip_all)] - #[allow(clippy::too_many_arguments)] pub async fn clone_variant( ctx: &DalContext, schema_variant_id: SchemaVariantId, - name: String, + schema_name: String, ) -> VariantAuthoringResult<(SchemaVariant, Schema)> { let variant = SchemaVariant::get_by_id(ctx, schema_variant_id).await?; let schema = variant.schema(ctx).await?; @@ -168,11 +189,11 @@ impl VariantAuthoringClient { if let Some(asset_func_id) = variant.asset_func_id() { let old_func = Func::get_by_id_or_error(ctx, asset_func_id).await?; - let cloned_func = old_func.duplicate(ctx, name.clone()).await?; + let cloned_func = old_func.duplicate(ctx, schema_name.clone()).await?; let cloned_func_spec = build_asset_func_spec(&cloned_func)?; let definition = execute_asset_func(ctx, &cloned_func).await?; let metadata = SchemaVariantMetadataJson { - schema_name: name.clone(), + schema_name: schema_name.clone(), name: "v0".into(), display_name: display_name.clone(), category: variant.category().to_string(), @@ -382,7 +403,7 @@ impl VariantAuthoringClient { .first() .ok_or(VariantAuthoringError::PkgMissingSchemaVariant)?; - let mut schema = SchemaVariant::get_by_id(ctx, current_schema_variant_id) + let schema = SchemaVariant::get_by_id(ctx, current_schema_variant_id) .await? .schema(ctx) .await?; @@ -403,7 +424,7 @@ impl VariantAuthoringClient { let mut thing_map = import_only_new_funcs(ctx, pkg.funcs()?).await?; if let Some(new_schema_variant) = import_schema_variant( ctx, - &mut schema, + &schema, schema_spec.clone(), variant_pkg_spec, None, @@ -427,7 +448,7 @@ impl VariantAuthoringClient { sv.category.clone_from(&category); sv.component_type = component_type; sv.color.clone_from(&color); - sv.display_name = Some(display_name.unwrap_or(schema_name.clone())); + sv.display_name = display_name.unwrap_or(schema_name.clone()); Ok(()) }) .await?; @@ -531,7 +552,7 @@ impl VariantAuthoringClient { .first() .ok_or(VariantAuthoringError::PkgMissingSchemaVariant)?; - let mut schema = SchemaVariant::get_by_id(ctx, current_sv_id) + let schema = SchemaVariant::get_by_id(ctx, current_sv_id) .await? .schema(ctx) .await?; @@ -545,9 +566,10 @@ impl VariantAuthoringClient { .await?; let mut thing_map = import_only_new_funcs(ctx, pkg.funcs()?).await?; + if let Some(new_schema_variant) = import_schema_variant( ctx, - &mut schema, + &schema, schema_spec.clone(), variant_pkg_spec, None, @@ -559,9 +581,102 @@ impl VariantAuthoringClient { schema .set_default_schema_variant(ctx, new_schema_variant.id) .await?; - return Ok(new_schema_variant.id); + + let new_sv_id = new_schema_variant.id; + + new_schema_variant.lock(ctx).await?; + + Ok(new_sv_id) } else { - return Err(VariantAuthoringError::NoAssetCreated); + Err(VariantAuthoringError::NoAssetCreated) + } + } + + // Note(victor): This is very similar to the logic in update_and_generate_variant_with_new_version, with a few differences: + // 1. it makes an exact copy of the schema variant - in the future there'll be no updates on unlocked copies + // 2. it does not update the default schema variant + #[instrument( + name = "variant.authoring.create_unlocked_variant_copy", + level = "info", + skip_all + )] + pub async fn create_unlocked_variant_copy( + ctx: &DalContext, + source_variant_id: SchemaVariantId, + ) -> VariantAuthoringResult { + let locked_variant = SchemaVariant::get_by_id(ctx, source_variant_id).await?; + let schema = locked_variant.schema(ctx).await?; + + // Create copy of asset func + let asset_func_id = locked_variant.asset_func_id().ok_or( + VariantAuthoringError::SchemaVariantAssetNotFound(locked_variant.id), + )?; + let unlocked_asset_func = Func::get_by_id_or_error(ctx, asset_func_id).await?; + + // Create new schema variant based on the asset func + let asset_func_spec = build_asset_func_spec(&unlocked_asset_func.clone())?; + let definition = execute_asset_func(ctx, &unlocked_asset_func).await?; + + let metadata = SchemaVariantMetadataJson { + schema_name: schema.name.clone(), + name: "VERSION GOES HERE".to_string(), + display_name: Some(format!("{}, unlocked", locked_variant.display_name.clone())), + category: locked_variant.category().to_string(), + color: locked_variant.color().to_string(), + component_type: locked_variant.component_type(), + link: locked_variant.link(), + description: locked_variant.description(), + }; + + let (unlocked_variant_spec, _skips, variant_funcs) = + build_variant_spec_based_on_existing_variant( + ctx, + definition, + &asset_func_spec, + &metadata, + source_variant_id, + ) + .await?; + + let schema_spec = metadata.to_schema_spec(unlocked_variant_spec)?; + + let creator_email = ctx.history_actor().email(ctx).await?; + let pkg_spec = PkgSpec::builder() + .name(&metadata.name) + .created_by(creator_email) + .funcs(variant_funcs.clone()) + .func(asset_func_spec) + .schema(schema_spec) + .version("0") + .build()?; + let pkg = SiPkg::load_from_spec(pkg_spec)?; + + let pkg_schemas = pkg.schemas()?; + let schema_spec = pkg_schemas + .first() + .ok_or(VariantAuthoringError::PkgMissingSchema)?; + + let pkg_variants = schema_spec.variants()?; + let variant_pkg_spec = pkg_variants + .first() + .ok_or(VariantAuthoringError::PkgMissingSchemaVariant)?; + + let mut thing_map = import_only_new_funcs(ctx, pkg.funcs()?).await?; + + if let Some(new_schema_variant) = import_schema_variant( + ctx, + &schema, + schema_spec.clone(), + variant_pkg_spec, + None, + &mut thing_map, + None, + ) + .await? + { + Ok(new_schema_variant) + } else { + Err(VariantAuthoringError::NoAssetCreated) } } @@ -575,7 +690,7 @@ impl VariantAuthoringClient { ctx: &DalContext, current_schema_variant_id: SchemaVariantId, schema_name: impl Into, - name: impl Into, + version: impl Into, display_name: Option, link: Option, code: impl Into, @@ -593,7 +708,8 @@ impl VariantAuthoringClient { VariantAuthoringError::SchemaVariantAssetNotFound(current_schema_variant_id), )?; - let name: String = name.into(); + // TODO rename this to version without breaking frontend + let name: String = version.into(); let name = &name; current_schema @@ -609,13 +725,15 @@ impl VariantAuthoringClient { current_schema_variant .modify(ctx, |sv| { - sv.name.clone_from(name); + sv.version.clone_from(name); sv.description = variant_description; sv.link = variant_link; sv.category.clone_from(&category.into()); sv.component_type = component_type; sv.color.clone_from(&color.into()); - sv.display_name = variant_display_name; + if let Some(display_name) = variant_display_name { + sv.display_name = display_name; + } Ok(()) }) .await?; diff --git a/lib/dal/src/schema/variant/metadata_view.rs b/lib/dal/src/schema/variant/metadata_view.rs index 940c08d694..3339dd81ed 100644 --- a/lib/dal/src/schema/variant/metadata_view.rs +++ b/lib/dal/src/schema/variant/metadata_view.rs @@ -11,10 +11,11 @@ pub struct SchemaVariantMetadataView { id: SchemaId, default_schema_variant_id: SchemaVariantId, schema_name: String, + // TODO rename this to version without breaking the frontend name: String, category: String, #[serde(alias = "display_name")] - display_name: Option, + display_name: String, color: String, component_type: ComponentType, link: Option, @@ -35,7 +36,7 @@ impl SchemaVariantMetadataView { id: schema.id, default_schema_variant_id: default_schema_variant.id, schema_name: schema.name.to_owned(), - name: default_schema_variant.name.to_owned(), + name: default_schema_variant.version.to_owned(), category: default_schema_variant.category.to_owned(), color: default_schema_variant.get_color(ctx).await?, timestamp: default_schema_variant.timestamp.to_owned(), diff --git a/lib/dal/src/schema/view.rs b/lib/dal/src/schema/view.rs index f8a2727af2..10d069e79a 100644 --- a/lib/dal/src/schema/view.rs +++ b/lib/dal/src/schema/view.rs @@ -64,7 +64,7 @@ impl SchemaView { // FIXME(nick): use the real value here builtin: true, is_default: schema_variant.id() == default_variant_id, - name: schema_variant.name().to_owned(), + name: schema_variant.version().to_owned(), color: schema_variant.get_color(ctx).await?, category: schema_variant.category().to_owned(), component_type: schema_variant.component_type().to_owned(), @@ -85,6 +85,7 @@ impl SchemaView { timestamp: schema_variant.timestamp(), description: schema_variant.description(), display_name: schema_variant.display_name(), + is_locked: schema_variant.is_locked(), }); } @@ -129,6 +130,7 @@ pub struct SchemaVariantView { timestamp: Timestamp, description: Option, display_name: Option, + is_locked: bool, } impl SchemaVariantView { diff --git a/lib/dal/src/socket/input.rs b/lib/dal/src/socket/input.rs index bd7ceb5708..611b852b74 100644 --- a/lib/dal/src/socket/input.rs +++ b/lib/dal/src/socket/input.rs @@ -63,6 +63,8 @@ pub enum InputSocketError { MultipleSocketsForAttributeValue(AttributeValueId), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), + #[error("input socket not found by name {0} in schema variant {1}")] + NotFoundByName(String, SchemaVariantId), #[error("schema variant error: {0}")] OutputSocketError(#[from] Box), #[error("schema variant error: {0}")] @@ -205,6 +207,17 @@ impl InputSocket { Ok(None) } + pub async fn find_with_name_or_error( + ctx: &DalContext, + name: impl AsRef, + schema_variant_id: SchemaVariantId, + ) -> InputSocketResult { + let name = name.as_ref(); + Self::find_with_name(ctx, name, schema_variant_id) + .await? + .ok_or_else(|| InputSocketError::NotFoundByName(name.to_string(), schema_variant_id)) + } + #[allow(clippy::too_many_arguments)] pub async fn new( ctx: &DalContext, @@ -459,4 +472,17 @@ impl InputSocket { } Ok(false) } + + pub async fn find_equivalent_in_schema_variant( + ctx: &DalContext, + input_socket_id: InputSocketId, + schema_variant_id: SchemaVariantId, + ) -> InputSocketResult { + let socket_name = Self::get_by_id(ctx, input_socket_id).await?.name; + Ok( + Self::find_with_name_or_error(ctx, socket_name, schema_variant_id) + .await? + .id, + ) + } } diff --git a/lib/dal/src/socket/output.rs b/lib/dal/src/socket/output.rs index 70297816f0..efac7496f2 100644 --- a/lib/dal/src/socket/output.rs +++ b/lib/dal/src/socket/output.rs @@ -56,6 +56,8 @@ pub enum OutputSocketError { NameCollision(OutputSocketId, OutputSocketId, SchemaVariantId), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), + #[error("output socket not found by name {0} in schema variant {1}")] + NotFoundByName(String, SchemaVariantId), #[error("schema variant error: {0}")] SchemaVariant(#[from] Box), #[error("transactions error: {0}")] @@ -368,6 +370,18 @@ impl OutputSocket { } Ok(maybe_output_socket) } + + pub async fn find_with_name_or_error( + ctx: &DalContext, + name: impl AsRef, + schema_variant_id: SchemaVariantId, + ) -> OutputSocketResult { + let name = name.as_ref(); + Self::find_with_name(ctx, name, schema_variant_id) + .await? + .ok_or_else(|| OutputSocketError::NotFoundByName(name.to_string(), schema_variant_id)) + } + #[instrument(level="debug" skip(ctx))] pub async fn fits_input_by_id( ctx: &DalContext, @@ -448,4 +462,17 @@ impl OutputSocket { Ok(results) } + + pub async fn find_equivalent_in_schema_variant( + ctx: &DalContext, + output_socket_id: OutputSocketId, + schema_variant_id: SchemaVariantId, + ) -> OutputSocketResult { + let socket_name = Self::get_by_id(ctx, output_socket_id).await?.name; + Ok( + Self::find_with_name_or_error(ctx, socket_name, schema_variant_id) + .await? + .id, + ) + } } diff --git a/lib/dal/tests/integration_test/schema/variant.rs b/lib/dal/tests/integration_test/schema/variant.rs index afb111b1e9..bdbb97f728 100644 --- a/lib/dal/tests/integration_test/schema/variant.rs +++ b/lib/dal/tests/integration_test/schema/variant.rs @@ -17,7 +17,7 @@ async fn new(ctx: &DalContext) { ctx, schema.id(), "ringo starr", - Some("ringo".to_string()), + "ringo".to_string(), "beatles", "#FFFFFF", ComponentType::Component, @@ -28,7 +28,7 @@ async fn new(ctx: &DalContext) { ) .await .expect("cannot create schema variant"); - assert_eq!(variant.name(), "ringo starr"); + assert_eq!(variant.version(), "ringo starr"); } #[test] @@ -38,7 +38,7 @@ async fn find_code_item_prop(ctx: &DalContext) { ctx, schema.id(), "v0", - Some("v0_display_name".to_string()), + "v0_display_name".to_string(), "demo", "#000000", ComponentType::Component, @@ -71,7 +71,7 @@ async fn list_root_si_child_props(ctx: &DalContext) { ctx, schema.id(), "v0", - Some("v0_display_name".to_string()), + "v0_display_name".to_string(), "demo", "#000000", ComponentType::Component, diff --git a/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs b/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs index bf24f4bdcb..e85ce676d1 100644 --- a/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs +++ b/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs @@ -20,7 +20,7 @@ async fn clone_variant(ctx: &mut DalContext) { .get_default_schema_variant_id(ctx) .await .expect("Unable to find the default schema variant id"); - let existing_schema = SchemaVariant::get_by_id( + let existing_variant = SchemaVariant::get_by_id( ctx, default_schema_variant.expect("unable to unwrap schema variant id"), ) @@ -32,12 +32,12 @@ async fn clone_variant(ctx: &mut DalContext) { let (new_schema_variant, _) = VariantAuthoringClient::clone_variant( ctx, default_schema_variant.expect("unable to get the schema variant id from the option"), - existing_schema.name().to_string(), + schema.name().to_string(), ) .await .expect("unable to clone the schema variant"); - assert_eq!(new_schema_variant.category(), existing_schema.category()); + assert_eq!(new_schema_variant.category(), existing_variant.category()); assert_eq!( new_schema_variant.display_name(), Some("dummy-secret Clone".to_string()) diff --git a/lib/sdf-server/src/server/service/graphviz.rs b/lib/sdf-server/src/server/service/graphviz.rs index f732be9c1a..e10b1b89b0 100644 --- a/lib/sdf-server/src/server/service/graphviz.rs +++ b/lib/sdf-server/src/server/service/graphviz.rs @@ -121,7 +121,7 @@ pub async fn schema_variant( id: sv_node_weight.id(), content_kind: sv_node_weight.content_address_discriminants(), node_kind: sv_node_weight.into(), - name: Some(sv.name().to_owned()), + name: Some(sv.version().to_owned()), } }; diff --git a/lib/sdf-server/src/server/service/variant.rs b/lib/sdf-server/src/server/service/variant.rs index f364621df6..33b85e888b 100644 --- a/lib/sdf-server/src/server/service/variant.rs +++ b/lib/sdf-server/src/server/service/variant.rs @@ -15,6 +15,7 @@ use thiserror::Error; use crate::server::state::AppState; pub mod clone_variant; +mod create_unlocked_copy; pub mod create_variant; pub mod get_variant; pub mod list_variants; @@ -26,6 +27,8 @@ pub mod update_variant; pub enum SchemaVariantError { #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), + #[error("trying to create unlocked copy for schema variant that's not the default: {0}")] + CreatingUnlockedCopyForNonDefault(SchemaVariantId), #[error("dal schema variant error: {0}")] DalSchemaVariant(#[from] dal::schema::variant::SchemaVariantError), #[error("func error: {0}")] @@ -90,4 +93,8 @@ pub fn routes() -> Router { .route("/update_variant", post(update_variant::update_variant)) .route("/clone_variant", post(clone_variant::clone_variant)) .route("/save_variant", post(save_variant::save_variant)) + .route( + "/create_unlocked_copy", + post(create_unlocked_copy::create_unlocked_copy), + ) } diff --git a/lib/sdf-server/src/server/service/variant/create_unlocked_copy.rs b/lib/sdf-server/src/server/service/variant/create_unlocked_copy.rs new file mode 100644 index 0000000000..d9c9657d52 --- /dev/null +++ b/lib/sdf-server/src/server/service/variant/create_unlocked_copy.rs @@ -0,0 +1,87 @@ +use crate::server::extract::{AccessBuilder, HandlerContext, PosthogClient}; +use crate::server::tracking::track; +use crate::service::variant::{SchemaVariantError, SchemaVariantResult}; +use axum::extract::OriginalUri; +use axum::{response::IntoResponse, Json}; +use dal::schema::variant::authoring::VariantAuthoringClient; +use dal::{ChangeSet, Schema, SchemaVariant, SchemaVariantId, Visibility, WsEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CloneVariantRequest { + pub id: SchemaVariantId, + #[serde(flatten)] + pub visibility: Visibility, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CloneVariantResponse { + pub id: SchemaVariantId, +} + +pub async fn create_unlocked_copy( + HandlerContext(builder): HandlerContext, + AccessBuilder(request_ctx): AccessBuilder, + PosthogClient(posthog_client): PosthogClient, + OriginalUri(original_uri): OriginalUri, + Json(request): Json, +) -> SchemaVariantResult { + let mut ctx = builder.build(request_ctx.build(request.visibility)).await?; + + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + let original_variant = SchemaVariant::get_by_id(&ctx, request.id).await?; + + let schema = original_variant.schema(&ctx).await?; + + if Schema::get_default_schema_variant_by_id(&ctx, schema.id()).await? + != Some(original_variant.id()) + { + return Err(SchemaVariantError::CreatingUnlockedCopyForNonDefault( + original_variant.id(), + )); + } + + let unlocked_variant = + VariantAuthoringClient::create_unlocked_variant_copy(&ctx, original_variant.id()).await?; + + track( + &posthog_client, + &ctx, + &original_uri, + "create_unlocked_variant_copy", + serde_json::json!({ + "schema_name": schema.name(), + "variant_version": original_variant.version(), + "original_variant_id": original_variant.id(), + "unlocked_variant_id": unlocked_variant.id(), + "variant_component_type": original_variant.component_type(), + }), + ); + + WsEvent::schema_variant_created( + &ctx, + unlocked_variant.schema(&ctx).await?.id(), + unlocked_variant.id(), + unlocked_variant.version().to_string(), + unlocked_variant.category().to_string(), + unlocked_variant.color().to_string(), + ) + .await? + .publish_on_commit(&ctx) + .await?; + + ctx.commit().await?; + + let mut response = axum::response::Response::builder(); + response = response.header("Content-Type", "application/json"); + if let Some(force_change_set_id) = force_change_set_id { + response = response.header("force_change_set_id", force_change_set_id.to_string()); + } + + Ok(response.body(serde_json::to_string(&CloneVariantResponse { + id: unlocked_variant.id(), + })?)?) +} diff --git a/lib/sdf-server/src/server/service/variant/get_variant.rs b/lib/sdf-server/src/server/service/variant/get_variant.rs index b61d7505f7..d04e3b0b6b 100644 --- a/lib/sdf-server/src/server/service/variant/get_variant.rs +++ b/lib/sdf-server/src/server/service/variant/get_variant.rs @@ -66,7 +66,7 @@ pub async fn get_variant( id: request.id, default_schema_variant_id, schema_name: schema.name().into(), - name: variant.name().into(), + name: variant.version().into(), display_name: variant.display_name(), category: variant.category().into(), color: variant.get_color(&ctx).await?, @@ -101,7 +101,7 @@ pub async fn get_variant( &original_uri, "get_variant", serde_json::json!({ - "variant_name": variant.name(), + "variant_version": variant.version(), "variant_category": variant.category(), "variant_display_name": variant.display_name(), "variant_id": variant.id(),