use serde_json::{json, Map, Value};
use crate::computes::{apply_defaults_and_computes, FnRegistry};
use crate::permission::{
can_write_schema, evaluate, filter_writable_data, merge_owner_condition, Context, Doc,
};
use crate::schema::{Registry, Schema};
use crate::types::{
has_relation_predicate, is_truthy, validate_condition, validate_condition_shape,
};
use super::cmd::{cmd_count_documents, cmd_find_one, cmd_insert_one};
use super::{ensure_context, ERR_NO_WRITE};
pub fn plan_insert(
schema_name: &str,
registry: &Registry,
ctx: Option<&Context>,
data: &Value,
now: i64,
new_id: &str,
fn_registry: Option<&dyn FnRegistry>,
) -> Result<Value, String> {
ensure_context(registry, ctx)?;
let schema = registry.get(schema_name)?;
if !can_write_schema(schema, ctx) {
return Err(ERR_NO_WRITE.to_string());
}
let doc = build_insert_doc(schema, ctx, data, now, new_id)?;
let doc = Value::Object(doc);
Ok(json!({
"command": cmd_insert_one(schema, &doc),
"returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
}))
}
pub(super) fn build_insert_doc(
schema: &Schema,
ctx: Option<&Context>,
data: &Value,
now: i64,
new_id: &str,
) -> Result<Map<String, Value>, String> {
let filtered = match ctx {
Some(c) => filter_writable_data(schema, Some(c), data),
None => data.clone(),
};
let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
if !doc.get("_id").map(is_truthy).unwrap_or(false) {
if schema.id_is_autoincrement() {
} else if !schema.id_prefix.is_empty() {
doc.insert("_id".to_string(), json!(new_id));
} else {
return Err(format!(
"schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
schema.name
));
}
}
if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
doc.insert("createdBy".to_string(), json!(uid));
}
}
if schema.timestamps {
if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
doc.insert("createdAt".to_string(), json!(now));
}
doc.insert("updatedAt".to_string(), json!(now));
}
Ok(doc)
}
pub fn plan_exists(
schema_name: &str,
registry: &Registry,
condition: &Value,
) -> Result<Value, String> {
validate_condition(condition)?;
let schema = registry.get(schema_name)?;
validate_condition_shape(schema, condition, registry.profile())?;
if has_relation_predicate(schema, condition) {
return Err(
"exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
请改用 query 并自行判断是否有结果"
.to_string(),
);
}
Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
}
pub fn plan_count(
schema_name: &str,
registry: &Registry,
filter: Option<&Value>,
ctx: Option<&Context>,
) -> Result<Value, String> {
if let Some(f) = filter {
validate_condition(f)?;
}
let schema = registry.get(schema_name)?;
if let Some(f) = filter {
validate_condition_shape(schema, f, registry.profile())?;
if has_relation_predicate(schema, f) {
return Err(
"count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
请改用 query 并在调用方自行统计"
.to_string(),
);
}
}
let base = match filter {
None | Some(Value::Null) => json!({}),
Some(v) => v.clone(),
};
let filter = merge_owner_condition(schema, ctx, Some(base)).unwrap_or_else(|| json!({}));
Ok(cmd_count_documents(schema, &filter))
}
pub(super) fn has_creator_permission(schema: &Schema) -> bool {
let has = |list: &Option<Vec<String>>| {
list.as_ref()
.map(|l| l.iter().any(|r| r == "creator"))
.unwrap_or(false)
};
has(&schema.read) || has(&schema.write)
}
#[derive(Debug, Clone, Copy)]
pub enum Probe<'a> {
NotProbed,
NoResult,
Found(&'a Value),
}
pub fn check_write_perm(
schema: &Schema,
ctx: Option<&Context>,
condition: &Value,
deny_msg: &str,
probe: Probe,
) -> Result<Option<Value>, String> {
let Some(c) = ctx else {
return Ok(None);
};
if c.roles
.clone()
.unwrap_or_default()
.iter()
.any(|r| r == "guest")
{
return Err(deny_msg.to_string());
}
if can_write_schema(schema, Some(c)) {
return Ok(None);
}
let creator_only = schema
.write
.as_ref()
.map(|w| w.iter().any(|r| r == "creator"))
.unwrap_or(false);
if creator_only && is_truthy(condition) {
return match probe {
Probe::NotProbed => Ok(Some(cmd_find_one(
schema,
condition,
Some(&json!({ "_id": 1, "createdBy": 1 })),
))),
Probe::NoResult => Err(deny_msg.to_string()),
Probe::Found(doc) => {
if evaluate(Some(c), schema.write.as_deref(), Doc::Doc(doc)) {
Ok(None)
} else {
Err(deny_msg.to_string())
}
}
};
}
Err(deny_msg.to_string())
}