Skip to content
143 changes: 25 additions & 118 deletions lib/luminork-server/src/service/v1/components/create_component.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
use axum::response::Json;
use dal::{
AttributeValue,
Component,
Prop,
Schema,
SchemaVariant,
attribute::attributes::AttributeSources,
cached_module::CachedModule,
diagram::view::View,
prop::PropPath,
};
use serde::{
Deserialize,
Serialize,
};
use serde_json::json;
use si_events::audit_log::AuditLogKind;
use si_id::ViewId;
use utoipa::{
self,
ToSchema,
};

use super::{
ComponentReference,
resolve_component_reference,
operations,
};
use crate::{
extract::{
Expand Down Expand Up @@ -68,132 +60,47 @@ pub async fn create_component(
return Err(ComponentsError::NotPermittedOnHead);
}

let schema_id =
match CachedModule::find_latest_for_schema_name(ctx, payload.schema_name.as_str()).await? {
Some(module) => module.schema_id,
None => match Schema::get_by_name_opt(ctx, payload.schema_name.as_str()).await? {
Some(schema) => schema.id(),
None => return Err(ComponentsError::SchemaNameNotFound(payload.schema_name)),
},
};
// Ensure that the schema is installed, get the default variant id
let mut variant_id = Schema::get_or_install_default_variant(ctx, schema_id).await?;

// Determine which variant to use based on use_working_copy flag
if payload.use_working_copy.unwrap_or(false) {
// User wants to use the unlocked (working copy) variant
match SchemaVariant::get_unlocked_for_schema(ctx, schema_id).await? {
Some(unlocked_variant) => {
// An unlocked variant already exists, use it
variant_id = unlocked_variant.id();
}
None => {
// No unlocked variant exists, so we should throw an error
return Err(ComponentsError::NoWorkingCopy(schema_id));
}
}
};

let variant = SchemaVariant::get_by_id(ctx, variant_id).await?;

let view_id: ViewId;
if let Some(view_name) = payload.view_name {
if let Some(view) = View::find_by_name(ctx, view_name.as_str()).await? {
view_id = view.id();
} else {
let view = View::new(ctx, view_name.as_str()).await?;
view_id = view.id()
}
// Lazy fetch component list only if managed_by is specified
let component_list = if !payload.managed_by.is_empty() {
Component::list_ids(ctx).await?
} else {
let default_view = View::get_id_for_default(ctx).await?;
view_id = default_view
vec![]
};

let mut component = Component::new(ctx, payload.name, variant_id, view_id).await?;
let comp_name = component.name(ctx).await?;
let initial_geometry = component.geometry(ctx, view_id).await?;
component
.set_geometry(
ctx,
view_id,
0,
0,
initial_geometry.width(),
initial_geometry.height(),
)
.await?;

tracker.track(
// Call core logic (includes audit logs, transactional)
let component_view = operations::create_component_core(
ctx,
"api_create_component",
json!({
"component_id": component.id(),
"schema_variant_id": variant_id,
"schema_variant_name": variant.display_name().to_string(),
"category": variant.category(),
}),
);
ctx.write_audit_log(
AuditLogKind::CreateComponent {
name: comp_name.clone(),
component_id: component.id(),
schema_variant_id: variant_id,
schema_variant_name: variant.display_name().to_string(),
},
comp_name.clone(),
payload.name,
payload.schema_name.clone(),
payload.view_name,
payload.resource_id,
payload.attributes.clone(),
payload.managed_by,
payload.use_working_copy,
&component_list,
)
.await?;

if !payload.attributes.is_empty() {
dal::update_attributes(ctx, component.id(), payload.attributes.clone()).await?;
}

let component_list = Component::list_ids(ctx).await?;
if let Some(resource_id) = payload.resource_id {
let resource_prop_path = ["root", "si", "resourceId"];
let resource_prop_id =
Prop::find_prop_id_by_path(ctx, variant_id, &PropPath::new(resource_prop_path)).await?;

let av_for_resource_id =
Component::attribute_value_for_prop_id(ctx, component.id(), resource_prop_id).await?;

AttributeValue::update(
ctx,
av_for_resource_id,
Some(serde_json::to_value(resource_id)?),
)
.await?;
}

if !payload.managed_by.is_empty() {
let manager_component_id =
resolve_component_reference(ctx, &payload.managed_by, &component_list).await?;

Component::manage_component(ctx, manager_component_id, component.id()).await?;
}

ctx.write_audit_log(
AuditLogKind::UpdateComponent {
component_id: component.id(),
component_name: comp_name.clone(),
},
comp_name.clone(),
)
.await?;
// Get variant info for tracking
let variant = SchemaVariant::get_by_id(ctx, component_view.schema_variant_id).await?;

// Track creation (non-transactional analytics)
tracker.track(
ctx,
"api_update_component",
"api_create_component",
json!({
"component_id": component.id(),
"component_name": comp_name.clone(),
"component_id": component_view.id,
"schema_variant_id": component_view.schema_variant_id,
"schema_variant_name": variant.display_name().to_string(),
"category": variant.category(),
}),
);

// Commit (publishes queued audit logs)
ctx.commit().await?;

Ok(Json(CreateComponentV1Response {
component: ComponentViewV1::assemble(ctx, component.id()).await?,
component: component_view,
}))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use axum::response::Json;
use dal::{
Component,
ComponentId,
};
use serde::{
Deserialize,
Serialize,
};
use serde_json::json;
use utoipa::{
self,
ToSchema,
};

use super::{
ComponentViewV1,
operations,
};
use crate::{
extract::{
PosthogEventTracker,
change_set::ChangeSetDalContext,
},
service::v1::{
ComponentsError,
components::create_component::CreateComponentV1Request,
},
};

#[derive(Deserialize, Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateManyComponentsV1Request {
pub components: Vec<CreateComponentV1Request>,
}

#[derive(Deserialize, Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateManyComponentsV1Response {
pub components: Vec<ComponentViewV1>,
}

#[utoipa::path(
post,
path = "/v1/w/{workspace_id}/change-sets/{change_set_id}/components/create_many",
params(
("workspace_id" = String, Path, description = "Workspace identifier"),
("change_set_id" = String, Path, description = "Change Set identifier"),
),
tag = "components",
request_body = CreateManyComponentsV1Request,
summary = "Create multiple components",
responses(
(status = 200, description = "Components created successfully", body = CreateManyComponentsV1Response),
(status = 400, description = "Bad Request - Not permitted on HEAD"),
(status = 401, description = "Unauthorized - Invalid or missing token"),
(status = 500, description = "Internal server error", body = crate::service::v1::common::ApiError)
)
)]
pub async fn create_many_components(
ChangeSetDalContext(ref ctx): ChangeSetDalContext,
posthog: PosthogEventTracker,
payload: Result<Json<CreateManyComponentsV1Request>, axum::extract::rejection::JsonRejection>,
) -> Result<Json<CreateManyComponentsV1Response>, ComponentsError> {
let Json(payload) = payload?;

// Validate not on HEAD change set
if ctx.change_set_id() == ctx.get_workspace_default_change_set_id().await? {
return Err(ComponentsError::NotPermittedOnHead);
}

// Lazy cache for component list (only fetch if needed by any request)
let mut component_list_cache: Option<Vec<ComponentId>> = None;
let mut results = Vec::with_capacity(payload.components.len());

// Process each component creation in order, stop on first error
for (index, request) in payload.components.iter().enumerate() {
// Lazy fetch component list only if this item needs it
if !request.managed_by.is_empty() && component_list_cache.is_none() {
component_list_cache = Some(Component::list_ids(ctx).await?);
}

let list = component_list_cache.as_deref().unwrap_or(&[]);

// Call core logic (includes audit logs, transactional)
let component = operations::create_component_core(
ctx,
request.name.clone(),
request.schema_name.clone(),
request.view_name.clone(),
request.resource_id.clone(),
request.attributes.clone(),
request.managed_by.clone(),
request.use_working_copy,
list,
)
.await
.map_err(|e| ComponentsError::BulkOperationFailed {
index,
source: Box::new(e),
})?;

results.push(component);
}

// Track bulk creation (non-transactional analytics)
posthog.track(
ctx,
"api_create_many_components",
json!({
"count": results.len(),
}),
);

// Commit (publishes queued audit logs transactionally)
ctx.commit().await?;

Ok(Json(CreateManyComponentsV1Response {
components: results,
}))
}
Loading