use std::collections::HashSet;
use serde_json::{json, Map, Value};
use crate::permission::{get_readable_fields, Context};
use crate::rbac::overlay_readable_fields;
use crate::schema::{ComputeDef, Registry, Schema};
use super::ast::Ast;
fn apply_permission_prune(
proj: &mut Map<String, Value>,
schema: &Schema,
ctx: Option<&Context>,
registry: &Registry,
) {
let Some(c) = ctx else { return };
let readable = get_readable_fields(registry.role_rules(), schema, Some(c));
let readable = overlay_readable_fields(registry, &schema.name, Some(c), readable);
let Some(readable) = readable else {
return;
};
let keys: Vec<String> = proj.keys().cloned().collect();
for key in keys {
if key == "_id" {
continue;
}
if schema.fields.contains_key(&key) && !readable.contains(&key) {
proj.remove(&key);
}
}
}
fn merge_compute_depends(
proj: &mut Map<String, Value>,
app_computes: &[&ComputeDef],
dot_parent_fields: &HashSet<String>,
) {
for c in app_computes {
for dep in &c.depends {
if dot_parent_fields.contains(dep) {
continue;
}
if dep.contains('{') {
let root = dep.split('{').next().unwrap_or(dep.as_str()).trim();
if !root.is_empty() {
proj.entry(root.to_string()).or_insert(json!(1));
}
continue;
}
proj.entry(dep.clone()).or_insert(json!(1));
}
}
}
fn append_compute_deps(
proj: &mut Map<String, Value>,
schema: &Schema,
dot_parent_fields: &HashSet<String>,
) {
let app_computes: Vec<&ComputeDef> = schema
.computes
.iter()
.map(|(_, c)| c)
.filter(|c| c.has_fn || c.has_async_fn)
.collect();
if app_computes.is_empty() {
return;
}
merge_compute_depends(proj, &app_computes, dot_parent_fields);
}
fn collect_real_fields(
ast: &Ast,
schema: &Schema,
compute_keys: &HashSet<String>,
) -> (Map<String, Value>, HashSet<String>, bool) {
let mut proj: Map<String, Value> = Map::new();
let mut dot_parent_fields: HashSet<String> = HashSet::new();
let mut any_field_requested: HashSet<String> = HashSet::new();
let mut has_real_field = false;
for f in &ast.fields {
if compute_keys.contains(f) {
continue;
}
if f.contains('.') {
let root = f.split('.').next().unwrap_or("").to_string();
if schema.fields.contains_key(&root) {
dot_parent_fields.insert(root.clone());
if !any_field_requested.contains(&root) {
proj.insert(f.clone(), json!(1));
}
has_real_field = true;
}
} else if schema.fields.contains_key(f) {
proj.insert(f.clone(), json!(1));
any_field_requested.insert(f.clone());
has_real_field = true;
}
}
if has_real_field {
for root in dot_parent_fields.iter().cloned().collect::<Vec<_>>() {
if proj.contains_key(&root) {
for key in proj.keys().cloned().collect::<Vec<_>>() {
if key.starts_with(&format!("{}.", root)) {
proj.remove(&key);
}
}
}
}
}
(proj, dot_parent_fields, has_real_field)
}
pub fn build_pipeline_projection(ast: &Ast, schema: &Schema) -> Option<Value> {
let mut proj: Map<String, Value> = Map::new();
proj.insert("_id".to_string(), json!(1));
let mut any = false;
let mut wants_score = false;
for f in &ast.fields {
if f == "_id" {
continue;
}
if f == "_score" {
wants_score = true;
continue;
}
if schema.fields.contains_key(f) {
proj.insert(f.clone(), json!(1));
any = true;
}
}
if wants_score {
proj.insert("_score".to_string(), json!(1));
}
if !any && !wants_score {
return None;
}
Some(Value::Object(proj))
}
pub fn build_projection(
ast: &Ast,
schema: &Schema,
ctx: Option<&Context>,
registry: &Registry,
) -> Option<Value> {
if ast.fields.is_empty() {
return None;
}
let compute_keys: HashSet<String> = schema.computes.iter().map(|(k, _)| k.clone()).collect();
let mut proj: Map<String, Value> = Map::new();
proj.insert("_id".to_string(), json!(1));
let (fields_proj, dot_parent_fields, has_real_field) =
collect_real_fields(ast, schema, &compute_keys);
for (k, v) in fields_proj {
proj.insert(k, v);
}
let wants_score = ast.fields.iter().any(|f| f == "_score");
if !has_real_field && !wants_score {
return None;
}
if wants_score {
proj.insert("_score".to_string(), json!(1));
}
append_compute_deps(&mut proj, schema, &dot_parent_fields);
let agg_compute_keys: HashSet<&str> = schema
.computes
.iter()
.filter(|(_, c)| !c.has_fn && !c.has_async_fn && c.agg.is_some())
.map(|(k, _)| k.as_str())
.collect();
for f in &ast.fields {
if agg_compute_keys.contains(f.as_str()) {
proj.insert(f.clone(), json!(1));
}
}
for (rel_name, _) in &ast.relations {
if !proj.contains_key(rel_name) {
proj.insert(rel_name.clone(), json!(1));
}
}
apply_permission_prune(&mut proj, schema, ctx, registry);
Some(Value::Object(proj))
}