use serde_json::{json, Map, Value};
use crate::computes::{apply_defaults_and_computes, FnRegistry};
use crate::permission::{can_write_schema, evaluate, merge_owner_condition, Context, Doc};
use crate::rbac::{
ensure_write, ensure_write_on_doc, filter_writable_data_overlay, merge_row_condition,
probe_condition_fields, WriteAction,
};
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::write_links::attach_write_links;
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(registry.role_rules(), schema, ctx) {
return Err(ERR_NO_WRITE.to_string());
}
ensure_write(registry, schema, ctx, WriteAction::Insert)?;
let doc = build_insert_doc(registry, schema, ctx, data, now, new_id)?;
let doc = Value::Object(doc);
let plan = json!({
"command": cmd_insert_one(schema, &doc),
"returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
});
attach_write_links(plan, registry, &[schema_name], registry.write_link_policy())
}
pub(super) fn build_insert_doc(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
data: &Value,
now: i64,
new_id: &str,
) -> Result<Map<String, Value>, String> {
let filtered = match ctx {
Some(_) => filter_writable_data_overlay(registry, schema, ctx, 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 owner_merged = merge_owner_condition(registry.role_rules(), schema, ctx, Some(base));
let filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)
.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),
}
#[allow(clippy::too_many_arguments)]
pub fn check_write_perm(
registry: &Registry,
schema: &Schema,
ctx: Option<&Context>,
condition: &Value,
deny_msg: &str,
probe: Probe,
action: WriteAction,
) -> Result<Option<Value>, String> {
let Some(c) = ctx else {
return Ok(None);
};
let rules = registry.role_rules();
if crate::permission::has_any_role(c, &rules.deny_write_roles) {
return Err(deny_msg.to_string());
}
ensure_write(registry, schema, Some(c), action)?;
let rbac_row_restricted =
crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
let probe_projection = |extra: &[String]| -> Value {
let mut m = Map::new();
m.insert("_id".to_string(), json!(1));
m.insert("createdBy".to_string(), json!(1));
for f in extra {
m.insert(f.clone(), json!(1));
}
Value::Object(m)
};
let rbac_fields = probe_condition_fields(registry, schema, Some(c), action.as_str());
if can_write_schema(registry.role_rules(), schema, Some(c)) {
if rbac_row_restricted && is_truthy(condition) {
return match probe {
Probe::NotProbed => Ok(Some(cmd_find_one(
schema,
condition,
Some(&probe_projection(&rbac_fields)),
))),
Probe::NoResult => Err(deny_msg.to_string()),
Probe::Found(doc) => {
ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
}
};
}
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) {
let needs_row_check = rbac_row_restricted;
return match probe {
Probe::NotProbed => Ok(Some(cmd_find_one(
schema,
condition,
Some(&probe_projection(&rbac_fields)),
))),
Probe::NoResult => Err(deny_msg.to_string()),
Probe::Found(doc) => {
if evaluate(
registry.role_rules(),
Some(c),
schema.write.as_deref(),
Doc::Doc(doc),
) {
if needs_row_check {
ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
} else {
Ok(None)
}
} else {
Err(deny_msg.to_string())
}
}
};
}
Err(deny_msg.to_string())
}