Skip to main content

rust_store_core/command/
count.rs

1//! 列表 + total 计划
2
3use serde_json::{json, Map, Value};
4
5use crate::permission::{merge_owner_condition, Context};
6use crate::pipeline::{param, parse_gql};
7use crate::rbac::merge_row_condition;
8use crate::schema::Registry;
9use crate::types::{has_relation_predicate, is_truthy};
10
11use super::cmd::{cmd_count_documents, num_value};
12use super::query::{plan_query_ast_mut, resolve_page, Page, QueryPlan};
13
14#[derive(Debug, Clone)]
15pub struct CountQueryPlan {
16    pub query: QueryPlan,
17    pub page: Page,
18    /// `total` 统计命令(忽略 skip/limit,但含所有者条件)
19    pub count_command: Value,
20}
21
22impl CountQueryPlan {
23    pub fn to_value(&self) -> Value {
24        let mut m = self
25            .query
26            .to_value()
27            .as_object()
28            .cloned()
29            .unwrap_or_default();
30        m.insert("countCommand".to_string(), self.count_command.clone());
31        if let Some(o) = self.page.to_value().as_object() {
32            for (k, v) in o {
33                m.insert(k.clone(), v.clone());
34            }
35        }
36        Value::Object(m)
37    }
38
39    /// `hasMore = (page + 1) * pageSize < total`
40    pub fn has_more(&self, total: f64) -> bool {
41        self.page.has_more(total)
42    }
43}
44
45/// 生成「列表 + total」命令序列(对应 JS `queryWithCount`)
46pub fn plan_query_with_count(
47    gql: &str,
48    params: &Map<String, Value>,
49    registry: &Registry,
50    ctx: Option<&Context>,
51) -> Result<CountQueryPlan, String> {
52    let mut ast = parse_gql(gql)?;
53    let schema = registry.get(&ast.model)?;
54
55    let mut params = params.clone();
56    let page = resolve_page(&ast, &params);
57
58    // R5:让 page/pageSize 真正落库为 LIMIT/OFFSET。
59    // 当用户经 params 传了 page/pageSize 而 GQL 未显式引用 $skip/$limit 时,
60    // 注入合成引用 + 参数,否则 build_pipeline 不产生分页阶段 → 返回全表(A-18 根因)。
61    let wants_pagination = params.contains_key("page") || params.contains_key("pageSize");
62    if wants_pagination && !ast.params.contains_key("skip") {
63        ast.params
64            .insert("skip".to_string(), "@__core_page_skip__".to_string());
65        params.insert(
66            "__core_page_skip__".to_string(),
67            num_value(page.page * page.page_size),
68        );
69    }
70    if wants_pagination && !ast.params.contains_key("limit") {
71        ast.params
72            .insert("limit".to_string(), "@__core_page_limit__".to_string());
73        params.insert("__core_page_limit__".to_string(), num_value(page.page_size));
74    }
75
76    // 确保 GQL 实际使用上述分页值(GQL 已引用 $skip/$limit 时用推导值覆盖,防宿主参数漂移)
77    if let Some(r) = ast.params.get("skip").cloned() {
78        let key = r.get(1..).unwrap_or("").to_string();
79        params.insert(key, num_value(page.page * page.page_size));
80    }
81    if let Some(r) = ast.params.get("limit").cloned() {
82        let key = r.get(1..).unwrap_or("").to_string();
83        params.insert(key, num_value(page.page_size));
84    }
85
86    // 统计 total:取(已被 owner 注入过的)条件后再叠加一次 owner 条件,与 JS 行为一致
87    let count_filter = ast
88        .params
89        .get("condition")
90        .and_then(|r| param(&params, Some(r)).cloned())
91        .filter(is_truthy)
92        .unwrap_or_else(|| json!({}));
93    // 静态 owner 条件叠加 RBAC 行条件(两引擎 $and,deny-wins)
94    let owner_merged =
95        merge_owner_condition(registry.role_rules(), schema, ctx, Some(count_filter));
96    let count_filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)
97        .unwrap_or_else(|| json!({}));
98
99    // §9.6 关系聚合谓词(`$condition` 中以**关系名**作键)无法用标量 `countDocuments` 表达:
100    // Mongo 会把它当成「字段等于该对象」、SQL 无对应列 → total 与 items 静默不一致。
101    // 显式 Err(D2:绝不静默给错数)。
102    if has_relation_predicate(schema, &count_filter) {
103        return Err(
104            "query_with_count 暂不支持关系聚合谓词条件(§9.6):total 无法与 items 保持一致,\
105             请改用 query 并在调用方自行统计"
106                .to_string(),
107        );
108    }
109
110    let query = plan_query_ast_mut(ast, &mut params, registry, ctx)?;
111
112    let count_command = cmd_count_documents(schema, &count_filter);
113
114    Ok(CountQueryPlan {
115        query,
116        page,
117        count_command,
118    })
119}