Skip to main content

sz_rust_core/plugin/
cross_query.rs

1//! 跨插件查询 — 通过 CapabilityRegistry 调用其他插件能力。
2//!
3//! 对应 design.md §2.2.2 接口 9。
4//! 自动注入 `tenant_id` 过滤,拒绝跨租户查询。
5
6use serde_json::Value;
7
8/// 跨插件查询错误。
9#[derive(Debug, thiserror::Error)]
10pub enum CrossQueryError {
11    /// 权限不足:目标租户与当前租户不一致
12    #[error("权限不足:租户 {tenant_id} 无权查询")]
13    PermissionDenied {
14        /// 当前(无权查询的)租户 ID
15        tenant_id: i64,
16    },
17    /// 能力未找到(CapabilityRegistry 无此能力)
18    #[error("能力未找到: {0}")]
19    NotFound(String),
20    /// 能力调用失败
21    #[error("查询失败: {0}")]
22    QueryFailed(String),
23}
24
25/// 跨插件查询。
26///
27/// 通过 `CapabilityRegistry::call_with_tenant` 调用其他插件能力,
28/// 自动注入 `tenant_id` 实现租户隔离。
29pub struct CrossQuery {
30    /// 当前租户 ID(所有查询强制注入)
31    tenant_id: i64,
32}
33
34impl CrossQuery {
35    /// 创建指定租户的跨插件查询实例。
36    pub fn new(tenant_id: i64) -> Self {
37        Self { tenant_id }
38    }
39
40    /// 返回当前租户 ID。
41    pub fn tenant_id(&self) -> i64 {
42        self.tenant_id
43    }
44
45    /// 构建带 tenant_id 的查询参数。
46    ///
47    /// 自动将 `tenant_id` 注入到查询参数中,确保租户隔离。
48    pub fn inject_tenant_filter(&self, mut args: Value) -> Value {
49        if let Some(obj) = args.as_object_mut() {
50            obj.insert("tenant_id".to_string(), Value::from(self.tenant_id));
51        } else if args.is_null() {
52            args = serde_json::json!({ "tenant_id": self.tenant_id });
53        }
54        args
55    }
56
57    /// 验证目标租户与当前租户一致。
58    ///
59    /// 拒绝跨租户查询,返回 `PermissionDenied`。
60    pub fn verify_tenant(&self, target_tenant_id: i64) -> Result<(), CrossQueryError> {
61        if self.tenant_id != target_tenant_id {
62            return Err(CrossQueryError::PermissionDenied {
63                tenant_id: self.tenant_id,
64            });
65        }
66        Ok(())
67    }
68
69    /// 构建聚合查询参数(批量查询多个能力)。
70    ///
71    /// `queries` 为 `(capability_name, args)` 元组列表,
72    /// 返回批量查询的 JSON 参数。
73    pub fn aggregate(&self, queries: &[(&str, Value)]) -> Value {
74        let queries_json: Vec<Value> = queries
75            .iter()
76            .map(|(name, args)| {
77                let injected = self.inject_tenant_filter(args.clone());
78                serde_json::json!({
79                    "capability": name,
80                    "args": injected,
81                })
82            })
83            .collect();
84        serde_json::json!({
85            "tenant_id": self.tenant_id,
86            "queries": queries_json,
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_inject_tenant_filter() {
97        let cq = CrossQuery::new(100);
98        let args = serde_json::json!({"keyword": "test"});
99        let result = cq.inject_tenant_filter(args);
100        assert_eq!(result["tenant_id"], 100);
101        assert_eq!(result["keyword"], "test");
102    }
103
104    #[test]
105    fn test_inject_tenant_filter_null() {
106        let cq = CrossQuery::new(200);
107        let result = cq.inject_tenant_filter(Value::Null);
108        assert_eq!(result["tenant_id"], 200);
109    }
110
111    #[test]
112    fn test_verify_tenant_same() {
113        let cq = CrossQuery::new(100);
114        assert!(cq.verify_tenant(100).is_ok());
115    }
116
117    #[test]
118    fn test_verify_tenant_different() {
119        let cq = CrossQuery::new(100);
120        assert!(cq.verify_tenant(200).is_err());
121    }
122
123    #[test]
124    fn test_aggregate() {
125        let cq = CrossQuery::new(100);
126        let queries = vec![
127            ("plugin_a.search", serde_json::json!({"q": "hello"})),
128            ("plugin_b.list", serde_json::json!({})),
129        ];
130        let result = cq.aggregate(&queries);
131        assert_eq!(result["tenant_id"], 100);
132        assert_eq!(result["queries"].as_array().unwrap().len(), 2);
133        assert_eq!(result["queries"][0]["args"]["tenant_id"], 100);
134    }
135}