use serde_json::{json, Map, Value};
use crate::command::cmd::cmd_find_one_and_update;
use crate::command::write::has_creator_permission;
use crate::command::{ensure_context, ERR_NO_WRITE};
use crate::permission::{can_write_schema, filter_writable_data, Context};
use crate::schema::{Registry, Schema};
use crate::types::{is_truthy, validate_condition, validate_condition_shape};
use super::{has_trim_str, object_of, remove_undefined};
pub(in crate::command) fn build_upsert_conditions(schema: &Schema, data: &Value) -> Vec<Value> {
let mut conditions = Vec::new();
if let Some(id) = data.get("_id") {
if has_trim_str(id) {
conditions.push(json!({ "_id": id }));
}
}
for idx in &schema.indexes {
let options = idx.get("options");
let unique = options.and_then(|o| o.get("unique"));
if !unique.map(is_truthy).unwrap_or(false) {
continue;
}
let Some(keys_obj) = idx.get("keys").and_then(|k| k.as_object()) else {
continue;
};
let all_present = keys_obj.keys().all(|k| match data.get(k) {
None => false,
Some(v) if v.is_null() => false,
Some(Value::String(s)) => !s.trim().is_empty(),
Some(_) => true,
});
if all_present {
let mut cond = Map::new();
for k in keys_obj.keys() {
cond.insert(k.clone(), data.get(k).cloned().unwrap_or(Value::Null));
}
conditions.push(Value::Object(cond));
}
}
conditions
}
pub(in crate::command) fn build_upsert_update(
schema: &Schema,
field_data: &Value,
new_id: &str,
now: i64,
) -> Value {
let mut set_data = object_of(&remove_undefined(field_data));
let mut set_on_insert = Map::new();
if let Some(id) = set_data.get("_id").cloned() {
if has_trim_str(&id) {
set_on_insert.insert("_id".to_string(), id);
}
} else if !schema.id_prefix.is_empty() {
set_on_insert.insert("_id".to_string(), json!(new_id));
}
set_data.remove("_id");
if schema.timestamps {
set_data.insert("updatedAt".to_string(), json!(now));
let created = field_data
.get("createdAt")
.cloned()
.unwrap_or_else(|| json!(now));
set_on_insert.insert("createdAt".to_string(), created);
}
set_data.remove("createdAt");
if has_creator_permission(schema)
&& !set_on_insert
.get("createdBy")
.map(is_truthy)
.unwrap_or(false)
{
if let Some(v) = set_on_insert.get("_id").cloned() {
set_on_insert.insert("createdBy".to_string(), v);
}
}
upsert_update_doc(set_data, set_on_insert)
}
pub(in crate::command) fn upsert_one_update(
schema: &Schema,
data: &Value,
new_id: &str,
now: i64,
) -> Value {
let mut set_data = object_of(&remove_undefined(data));
let mut set_on_insert = Map::new();
if let Some(id) = set_data.get("_id").cloned() {
if has_trim_str(&id) {
set_on_insert.insert("_id".to_string(), id);
}
} else if !schema.id_prefix.is_empty() {
set_on_insert.insert("_id".to_string(), json!(new_id));
}
set_data.remove("_id");
if schema.timestamps {
set_data.insert("updatedAt".to_string(), json!(now));
let created = data.get("createdAt").cloned().unwrap_or_else(|| json!(now));
set_on_insert.insert("createdAt".to_string(), created);
}
set_data.remove("createdAt");
upsert_update_doc(set_data, set_on_insert)
}
fn upsert_update_doc(set_data: Map<String, Value>, set_on_insert: Map<String, Value>) -> Value {
let mut update = Map::new();
update.insert("$set".to_string(), Value::Object(set_data));
if !set_on_insert.is_empty() {
update.insert("$setOnInsert".to_string(), Value::Object(set_on_insert));
}
Value::Object(update)
}
#[allow(clippy::too_many_arguments)]
pub fn plan_upsert(
schema_name: &str,
registry: &Registry,
ctx: Option<&Context>,
condition: &Value,
data: &Value,
options: &Value,
now: i64,
new_id: &str,
) -> Result<Value, String> {
ensure_context(registry, ctx)?;
let schema = registry.get(schema_name)?;
validate_condition(condition)?;
validate_condition_shape(schema, condition, registry.profile())?;
if !can_write_schema(schema, ctx) {
return Err(ERR_NO_WRITE.to_string());
}
let filtered_data = match ctx {
Some(_) => filter_writable_data(schema, ctx, data),
None => data.clone(),
};
let return_new = options
.as_object()
.and_then(|o| o.get("returnNew"))
.map(is_truthy)
.unwrap_or(true);
let mut set_data = object_of(&remove_undefined(&filtered_data));
let mut set_on_insert = Map::new();
if let Some(id) = set_data.get("_id").cloned() {
if has_trim_str(&id) {
set_on_insert.insert("_id".to_string(), id);
}
} else if !schema.id_prefix.is_empty() && !condition.get("_id").map(is_truthy).unwrap_or(false)
{
set_on_insert.insert("_id".to_string(), json!(new_id));
}
set_data.remove("_id");
if schema.timestamps {
set_data.insert("updatedAt".to_string(), json!(now));
let created = filtered_data
.get("createdAt")
.cloned()
.unwrap_or_else(|| json!(now));
set_on_insert.insert("createdAt".to_string(), created);
}
set_data.remove("createdAt");
if has_creator_permission(schema)
&& !set_on_insert
.get("createdBy")
.map(is_truthy)
.unwrap_or(false)
{
let idv = match set_on_insert.get("_id") {
Some(v) if is_truthy(v) => Some(v.clone()),
_ => condition.get("_id").cloned(),
};
if let Some(v) = idv {
set_on_insert.insert("createdBy".to_string(), v);
}
}
let update_doc = upsert_update_doc(set_data, set_on_insert);
let fu_options = json!({
"upsert": true,
"returnDocument": if return_new { "after" } else { "before" },
});
let command = cmd_find_one_and_update(schema, condition, &update_doc, &fu_options);
Ok(json!({ "command": command }))
}