#[cfg(server)]
use super::admin_auth::AdminAuthenticatedUser;
#[cfg(server)]
use crate::adapters::{AdminDatabase, AdminSite};
#[cfg(server)]
use crate::core::database::canonicalize_pk_value;
#[cfg(server)]
use crate::core::history::insert_history_event;
#[cfg(server)]
use crate::core::{AdminDatabaseKey, AdminSiteKey};
use crate::types::MutationResponse;
#[cfg(server)]
use reinhardt_di::KeyedDepends;
#[cfg(server)]
use reinhardt_pages::server_fn::ServerFnRequest;
use reinhardt_pages::server_fn::{ServerFnError, server_fn};
#[cfg(server)]
use std::collections::{HashMap, HashSet};
#[cfg(server)]
use super::audit;
#[cfg(server)]
use super::error::{AdminAuth, MapServerFnError, ModelPermission};
#[cfg(server)]
use super::form::prepare_parent_form_data;
#[cfg(server)]
use super::inline::{
map_inline_mutation_error, map_inline_transaction_error, parse_inline_mutations,
preflight_inline_permissions, remove_unchanged_inline_mutations,
sanitize_inline_mutations_with_trusted_fields, save_inline_mutations,
};
#[cfg(server)]
use super::limits::MAX_RELATION_SELECTIONS;
#[cfg(server)]
use super::relation::{
lock_relation_source, relation_selection_is_unchanged, relation_value, resolve_relations,
split_relation_values, sync_relation_ids, validate_relation_ids, validate_relation_values,
};
#[cfg(server)]
use super::security::require_csrf_token;
#[cfg(all(server, not(feature = "file-uploads")))]
use super::security::sanitize_mutation_values;
#[cfg(server)]
use super::type_inference::{
translate_logical_field_names, translate_physical_field_names_to_logical,
};
#[server_fn]
pub async fn update_record(
model_name: String,
id: String,
request: crate::types::MutationRequest,
#[inject] site: KeyedDepends<AdminSiteKey, AdminSite>,
#[inject] db: KeyedDepends<AdminDatabaseKey, AdminDatabase>,
#[inject] http_request: ServerFnRequest,
#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
) -> Result<crate::types::MutationResponse, ServerFnError> {
#[cfg(feature = "file-uploads")]
let cleanup_site = site.as_ref().clone();
let (response, _, outcomes) = update_record_with_previous_values(
model_name,
id,
request,
site,
db,
http_request,
AdminAuthenticatedUser(user),
)
.await?;
#[cfg(feature = "file-uploads")]
super::multipart::schedule_inline_delete_cleanups(cleanup_site, outcomes).await;
#[cfg(not(feature = "file-uploads"))]
let _ = outcomes;
Ok(response)
}
#[cfg(server)]
pub(crate) async fn update_record_with_previous_values(
model_name: String,
id: String,
request: crate::types::MutationRequest,
site: KeyedDepends<AdminSiteKey, AdminSite>,
db: KeyedDepends<AdminDatabaseKey, AdminDatabase>,
http_request: ServerFnRequest,
user: AdminAuthenticatedUser,
) -> Result<
(
crate::types::MutationResponse,
HashMap<String, serde_json::Value>,
Vec<super::inline::InlineSaveOutcome>,
),
ServerFnError,
> {
update_record_with_trusted_file_fields(
model_name,
id,
request,
site,
db,
http_request,
user,
&HashSet::new(),
)
.await
}
#[cfg(server)]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn update_record_with_trusted_file_fields(
model_name: String,
id: String,
request: crate::types::MutationRequest,
site: KeyedDepends<AdminSiteKey, AdminSite>,
db: KeyedDepends<AdminDatabaseKey, AdminDatabase>,
http_request: ServerFnRequest,
AdminAuthenticatedUser(user): AdminAuthenticatedUser,
trusted_file_fields: &HashSet<String>,
) -> Result<
(
crate::types::MutationResponse,
HashMap<String, serde_json::Value>,
Vec<super::inline::InlineSaveOutcome>,
),
ServerFnError,
> {
require_csrf_token(&request.csrf_token, &http_request.inner().headers)?;
let auth = AdminAuth::from_request(&http_request);
let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::Change)
.await?;
#[cfg(feature = "file-uploads")]
super::multipart::reject_file_field_json_data_with_trusted_fields(
&request.data,
model_admin.as_ref(),
site.as_ref(),
trusted_file_fields,
)?;
let model_name = model_admin.model_name().to_string();
let table_name = model_admin.table_name().to_string();
let pk_field = model_admin.pk_field().to_string();
let object_id = canonicalize_pk_value(&table_name, &pk_field, &id);
let inlines = model_admin.inlines();
let mut request = request;
let mut inline_mutations = if inlines.is_empty() {
Vec::new()
} else {
parse_inline_mutations(&mut request.data, &inlines).map_err(map_inline_mutation_error)?
};
let data = prepare_parent_form_data(
&site,
model_admin.as_ref(),
crate::core::AdminFormMode::Update,
request.data,
)?;
let descriptors = resolve_relations(&site, model_admin.as_ref()).map_server_fn_error()?;
let (mut data, selections) = split_relation_values(data, &descriptors).map_server_fn_error()?;
for selection in &selections {
auth.require_model_permission(
selection.descriptor.target_admin.as_ref(),
user.as_ref(),
ModelPermission::View,
)
.await?;
}
let relation_values =
validate_relation_values(&auth, user.as_ref(), &site, &db, &model_admin, &mut data).await?;
let mut sanitized_data = data;
#[cfg(feature = "file-uploads")]
super::security::sanitize_mutation_values_with_trusted_fields(
&mut sanitized_data,
trusted_file_fields,
);
#[cfg(not(feature = "file-uploads"))]
sanitize_mutation_values(&mut sanitized_data);
sanitize_inline_mutations_with_trusted_fields(&mut inline_mutations, trusted_file_fields);
sanitized_data.extend(relation_values);
let mut audit_data = sanitized_data.clone();
for selection in &selections {
audit_data.insert(
selection.descriptor.field_name.clone(),
serde_json::Value::Null,
);
}
super::create::inject_auto_now_timestamps(&mut sanitized_data, &table_name);
translate_logical_field_names(&table_name, &mut sanitized_data).map_server_fn_error()?;
let actor = user.get_username().to_string();
let audit_user_id = auth.user_id().unwrap_or("unknown").to_string();
let mut connection = *db.connection();
if !inlines.is_empty() {
remove_unchanged_inline_mutations(
&inlines,
&object_id,
&mut inline_mutations,
&mut connection,
)
.await
.map_err(map_inline_mutation_error)?;
preflight_inline_permissions(
&auth,
site.as_ref(),
user.as_ref(),
&inlines,
&inline_mutations,
)
.await?;
}
let result: Result<_, super::inline::InlineTransactionError> = async {
connection
.atomic_write(async |transaction| {
let current_data = db
.get_with_executor_for_update(transaction, &table_name, &pk_field, &object_id)
.await?;
let Some(current_data) = current_data else {
return Err(crate::types::AdminError::ModelNotRegistered(format!(
"{} not found",
model_name
))
.into());
};
if let Some(selection) = selections.first() {
lock_relation_source(transaction, &selection.descriptor, &object_id)
.await
.map_err(reinhardt_core::exception::Error::from)?;
}
let mut relation_changed_fields = Vec::new();
for selection in &selections {
let source_pk = relation_value(
&selection.descriptor.source_metadata,
&selection.descriptor.source_pk_field,
&object_id,
)
.map_err(reinhardt_core::exception::Error::from)?;
if selection.ids.len() > MAX_RELATION_SELECTIONS {
let unchanged = relation_selection_is_unchanged(
transaction,
&selection.descriptor,
&source_pk,
&selection.ids,
)
.await
.map_err(reinhardt_core::exception::Error::from)?;
if !unchanged {
return Err(crate::types::AdminError::ValidationError(format!(
"Field '{}' relation selection too large: {} elements (max {})",
selection.descriptor.field_name,
selection.ids.len(),
MAX_RELATION_SELECTIONS
))
.into());
}
continue;
}
validate_relation_ids(transaction, &selection.descriptor, &selection.ids)
.await
.map_err(reinhardt_core::exception::Error::from)?;
if sync_relation_ids(
transaction,
&selection.descriptor,
source_pk,
&selection.ids,
)
.await
.map_err(reinhardt_core::exception::Error::from)?
{
relation_changed_fields.push(selection.descriptor.field_name.clone());
}
}
let mut changed_fields = sanitized_data
.iter()
.filter(|&(field, value)| current_data.get(field) != Some(value))
.map(|(field, _value)| field.clone())
.collect::<Vec<_>>();
changed_fields.extend(relation_changed_fields);
changed_fields.sort_unstable();
changed_fields.dedup();
let affected = if sanitized_data.is_empty() {
0
} else {
db.update_with_executor(
transaction,
&table_name,
&pk_field,
&object_id,
sanitized_data.clone(),
)
.await?
};
let outcomes =
save_inline_mutations(&inlines, &object_id, inline_mutations, transaction)
.await?;
if !changed_fields.is_empty() {
let event = audit::new_history_event(
&actor,
"UPDATE",
&model_name,
&table_name,
&object_id,
changed_fields.clone(),
affected,
);
insert_history_event(transaction, &event).await?;
}
super::inline::insert_inline_history_events(
site.as_ref(),
&actor,
&outcomes,
transaction,
)
.await?;
Ok((affected, current_data, outcomes))
})
.await
}
.await;
let (affected, mut previous_data, outcomes) = match result {
Err(error) => {
audit::log_update(&audit_user_id, &model_name, &id, &audit_data, false);
return Err(map_inline_transaction_error(error));
}
Ok((affected, previous_data, outcomes)) => (affected, previous_data, outcomes),
};
audit::log_update(&audit_user_id, &model_name, &id, &audit_data, true);
audit::log_inline_outcomes(site.as_ref(), &audit_user_id, &outcomes);
translate_physical_field_names_to_logical(&table_name, &mut previous_data)
.map_server_fn_error()?;
let response = MutationResponse {
success: true,
message: format!("{} updated successfully", model_name),
affected: Some(affected),
data: None,
};
Ok((response, previous_data, outcomes))
}