use std::collections::HashSet;
use serde_json::{json, Map, Value};
use crate::command::{ERR_PERMISSION, ERR_TEXT2QUERY, T2Q_MAX_DEPTH};
use crate::permission::{
can_read_schema, get_readable_computes, is_field_readable, is_relation_readable,
merge_owner_condition, Context,
};
use crate::rbac::{ensure_read, overlay_readable_fields, row_condition};
use crate::schema::{Profile, Registry, RelationDef, Schema};
use crate::types::{validate_condition, validate_condition_shape, validate_sort_shape};
use super::ast::RelAst;
use super::util::{append_order, clamp_t2q_limit, is_nullish, non_nullish, param};
use super::{MAX_DEPTH, MAX_PAGINATED_DEPTH};
pub(crate) fn push_row_conditions(
ands: &mut Vec<Value>,
rel_schema: &Schema,
registry: &Registry,
ctx: Option<&Context>,
) {
if let Some(owner) = merge_owner_condition(registry.role_rules(), rel_schema, ctx, None) {
ands.push(owner);
}
if let Some(rbac_cond) = row_condition(registry, rel_schema, ctx, "read") {
ands.push(rbac_cond);
}
}
fn readable_computes_overlay(
schema: &Schema,
registry: &Registry,
ctx: Option<&Context>,
) -> Option<HashSet<String>> {
let base = get_readable_computes(registry.role_rules(), schema, ctx);
overlay_readable_fields(registry, &schema.name, ctx, base)
}
pub(crate) fn rel_match_expr(foreign_key: &str, let_var: &str, is_array_field: bool) -> Value {
if is_array_field {
json!({ "$expr": { "$in": [format!("${}", foreign_key), format!("$${}", let_var)] } })
} else {
json!({ "$expr": { "$eq": [format!("${}", foreign_key), format!("$${}", let_var)] } })
}
}
pub(crate) fn rel_let_expr(local_key: &str, is_array_field: bool) -> Value {
if is_array_field {
json!({ "$cond": [{ "$isArray": format!("${}", local_key) }, format!("${}", local_key), []] })
} else {
json!({ "$ifNull": [format!("${}", local_key), null] })
}
}
pub(crate) fn is_array_local_field(schema: &Schema, local_key: &str) -> bool {
schema
.fields
.get(local_key)
.map(|f| f.field_type == "array")
.unwrap_or(false)
}
pub fn build_empty_lookup(
rel_name: &str,
rel_def: &RelationDef,
rel_schema: &Schema,
source_schema: &Schema,
) -> Value {
let local_key = &rel_def.local_field;
let let_var = format!("rel_{}", local_key);
let is_array = is_array_local_field(source_schema, local_key);
let match_expr = rel_match_expr(&rel_def.foreign_field, &let_var, is_array);
let mut let_map = Map::new();
let_map.insert(let_var, rel_let_expr(local_key, is_array));
let mut lookup = Map::new();
lookup.insert(
"from".to_string(),
Value::String(rel_schema.collection.clone()),
);
lookup.insert("let".to_string(), Value::Object(let_map));
lookup.insert("pipeline".to_string(), json!([{ "$match": match_expr }]));
lookup.insert("as".to_string(), Value::String(rel_name.to_string()));
json!({ "$lookup": Value::Object(lookup) })
}
fn build_rel_projection(rel_ast: &RelAst, rel_schema: &Schema) -> Option<Value> {
if rel_ast.fields.is_empty() {
return None;
}
let mut proj = Map::new();
proj.insert("_id".to_string(), json!(1));
for f in &rel_ast.fields {
proj.insert(f.clone(), json!(1));
}
for (n_name, _) in &rel_ast.relations {
proj.insert(n_name.clone(), json!(1));
}
for f in &rel_ast.fields {
if let Some(comp) = rel_schema.compute(f) {
if comp.has_fn {
for dep in &comp.depends {
proj.entry(dep.clone()).or_insert(json!(1));
}
}
}
}
Some(Value::Object(proj))
}
fn ns_lookup_stages(
rel_ast: &RelAst,
rel_schema: &Schema,
params: &Map<String, Value>,
depth: usize,
next_paginated: usize,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<Vec<Value>, String> {
let mut stages = Vec::new();
for (n_name, n_ast) in &rel_ast.relations {
let n_def = rel_schema.relations.get(n_name).ok_or_else(|| {
format!(
"关系 \"{}\" 未在 schema \"{}\" 中定义",
n_name, rel_schema.name
)
})?;
let n_schema = registry.get(&n_def.model)?;
stages.push(build_lookup(
n_name,
n_ast,
params,
n_def,
n_schema,
rel_schema,
depth + 1,
next_paginated,
registry,
ctx,
)?);
if n_def.rel_type == "one" {
stages.push(json!({
"$unwind": { "path": format!("${}", n_name), "preserveNullAndEmptyArrays": true }
}));
}
}
Ok(stages)
}
#[allow(clippy::too_many_arguments)]
pub fn build_lookup(
rel_name: &str,
rel_ast: &RelAst,
params: &Map<String, Value>,
rel_def: &RelationDef,
rel_schema: &Schema,
source_schema: &Schema,
depth: usize,
paginated: usize,
registry: &Registry,
ctx: Option<&Context>,
) -> Result<Value, String> {
let local_key = rel_def.local_field.clone();
let foreign_key = rel_def.foreign_field.clone();
let let_var = format!("rel_{}", local_key);
let condition = param(params, rel_ast.params.get("condition")).cloned();
let sort = param(params, rel_ast.params.get("sort")).cloned();
let skip_val = param(params, rel_ast.params.get("skip")).cloned();
let limit_val = clamp_t2q_limit(registry, param(params, rel_ast.params.get("limit")));
let has_paginated = !is_nullish(skip_val.as_ref()) || !is_nullish(limit_val.as_ref());
let next_paginated = if has_paginated {
paginated + 1
} else {
paginated
};
let max_depth = if registry.profile() == Profile::Text2Query {
T2Q_MAX_DEPTH
} else {
MAX_DEPTH
};
if depth >= max_depth || (has_paginated && paginated >= MAX_PAGINATED_DEPTH) {
if registry.profile() == Profile::Text2Query {
return Err(format!(
"{ERR_TEXT2QUERY}text2query 档关系嵌套超限(深度 {depth} ≥ {T2Q_MAX_DEPTH} / 分页深度 {paginated} ≥ {MAX_PAGINATED_DEPTH})"
));
}
return Err(format!(
"关系嵌套超限(深度 {depth} ≥ {MAX_DEPTH} / 分页深度 {paginated} ≥ {MAX_PAGINATED_DEPTH})——拒绝静默降级"
));
}
let is_array = is_array_local_field(source_schema, &local_key);
let mut stages: Vec<Value> = Vec::new();
let match_expr = rel_match_expr(&foreign_key, &let_var, is_array);
let mut ands: Vec<Value> = vec![match_expr];
if let Some(cond) = non_nullish(condition.as_ref()) {
validate_condition(cond)?;
validate_condition_shape(rel_schema, cond, registry.profile())?;
ands.push(cond.clone());
}
push_row_conditions(&mut ands, rel_schema, registry, ctx);
let match_doc = if ands.len() == 1 {
ands.remove(0)
} else {
json!({ "$and": ands })
};
stages.push(json!({ "$match": match_doc }));
if let Some(srt) = non_nullish(sort.as_ref()) {
validate_sort_shape(rel_schema, srt, registry.profile())?;
}
let sorts_by_nested = sort
.as_ref()
.and_then(|s| s.as_object())
.map(|o| o.keys().any(|k| k.contains('.')))
.unwrap_or(false);
if !sorts_by_nested {
append_order(
&mut stages,
sort.as_ref(),
skip_val.as_ref(),
limit_val.as_ref(),
);
}
stages.extend(ns_lookup_stages(
rel_ast,
rel_schema,
params,
depth,
next_paginated,
registry,
ctx,
)?);
if sorts_by_nested {
append_order(
&mut stages,
sort.as_ref(),
skip_val.as_ref(),
limit_val.as_ref(),
);
}
if let Some(rel_proj) = build_rel_projection(rel_ast, rel_schema) {
stages.push(json!({ "$project": rel_proj }));
}
let mut let_map = Map::new();
let_map.insert(let_var, rel_let_expr(&local_key, is_array));
let mut lookup = Map::new();
lookup.insert(
"from".to_string(),
Value::String(rel_schema.collection.clone()),
);
lookup.insert("let".to_string(), Value::Object(let_map));
lookup.insert("pipeline".to_string(), Value::Array(stages));
lookup.insert("as".to_string(), Value::String(rel_name.to_string()));
Ok(json!({ "$lookup": Value::Object(lookup) }))
}
pub fn build_agg_stages(
schema: &Schema,
registry: &Registry,
ctx: Option<&Context>,
requested: &HashSet<String>,
) -> Result<Vec<Value>, String> {
let readable = if ctx.is_some() {
readable_computes_overlay(schema, registry, ctx)
} else {
None
};
let mut stages: Vec<Value> = Vec::new();
let mut add_fields = Map::new();
for (key, comp) in &schema.computes {
if !requested.contains(key) {
continue;
}
if let Some(set) = &readable {
if !set.contains(key) {
continue;
}
}
let Some(agg) = &comp.agg else { continue };
let (op, path) = agg
.as_object()
.and_then(|o| o.iter().next())
.ok_or_else(|| {
format!("计算列 \"{key}\" 的 agg 定义非法(须为 {{\"$op\": \"<关系路径>\"}})")
})?;
let path = path.as_str().unwrap_or("");
let (rel_name, field) = match path.split_once('.') {
Some((r, f)) => (r.to_string(), Some(f.to_string())),
None => (path.to_string(), None),
};
let rel_def = schema.relations.get(&rel_name).ok_or_else(|| {
format!("计算列 \"{key}\" 的 agg 引用的关系 \"{rel_name}\" 未在 schema 中定义")
})?;
let rel_schema = registry.get(&rel_def.model)?;
if ctx.is_some() {
let field_unreadable = field
.as_deref()
.map(|f| {
!is_field_readable(registry.role_rules(), rel_schema, ctx, f)
|| !crate::rbac::is_field_readable_overlay(registry, rel_schema, ctx, f)
})
.unwrap_or(false);
if !is_relation_readable(registry.role_rules(), schema, ctx, &rel_name)
|| !can_read_schema(registry.role_rules(), rel_schema, ctx)
|| field_unreadable
{
return Err(ERR_PERMISSION.to_string());
}
ensure_read(registry, rel_schema, ctx)?;
}
let as_name = format!("_{}", key);
let let_var = "aggv";
let is_array = is_array_local_field(schema, &rel_def.local_field);
let mut let_map = Map::new();
let_map.insert(
let_var.to_string(),
rel_let_expr(&rel_def.local_field, is_array),
);
let mut ands: Vec<Value> = vec![rel_match_expr(&rel_def.foreign_field, let_var, is_array)];
push_row_conditions(&mut ands, rel_schema, registry, ctx);
let match_doc = if ands.len() == 1 {
ands.remove(0)
} else {
json!({ "$and": ands })
};
let mut inner = Map::new();
inner.insert(
"from".to_string(),
Value::String(rel_schema.collection.clone()),
);
inner.insert("let".to_string(), Value::Object(let_map));
inner.insert("pipeline".to_string(), json!([{ "$match": match_doc }]));
inner.insert("as".to_string(), Value::String(as_name.clone()));
stages.push(json!({ "$lookup": Value::Object(inner) }));
let arr = json!({ "$ifNull": [format!("${}", as_name), []] });
let field_ref = format!("${}.{}", as_name, field.clone().unwrap_or_default());
match op.as_str() {
"$count" => {
add_fields.insert(key.clone(), json!({ "$size": arr }));
}
"$sum" => {
add_fields.insert(
key.clone(),
json!({ "$cond": [{ "$eq": [{ "$size": arr }, 0] }, null, { "$sum": field_ref }] }),
);
}
"$avg" => {
add_fields.insert(key.clone(), json!({ "$avg": field_ref }));
}
"$min" => {
add_fields.insert(key.clone(), json!({ "$min": field_ref }));
}
"$max" => {
add_fields.insert(key.clone(), json!({ "$max": field_ref }));
}
other => {
return Err(format!(
"计算列 \"{key}\" 的 agg 算子 {other} 不在白名单($count/$sum/$avg/$min/$max)"
));
}
}
}
if !add_fields.is_empty() {
stages.push(json!({ "$addFields": Value::Object(add_fields) }));
}
Ok(stages)
}