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