Skip to main content

rust_store_core/schema/
registry.rs

1//! Schema 注册表:注册/定位/配置(数据结构与规范化见 [`super::definition`])。
2
3use std::collections::HashMap;
4
5use serde_json::{json, Map, Value};
6
7use crate::types::{is_truthy, str_list, AGG_OPS};
8
9use super::definition::{normalize_fields, ComputeDef, FieldDef, RelationDef, Schema};
10
11/// 查询档位:判决唯一在 core(照 [`Registry::require_context`] 既有范式)。
12///
13/// - [`Profile::Standard`](默认):标准调用 —— 跨方言对齐的公共能力集;
14///   DB 独有能力可用但须代码注释标注「不建议业务查询」(见执行文档 §4.5)。
15/// - [`Profile::Text2Query`]:AI 问数 —— 功能收缩 + 硬性限制(行数/深度/准入全收紧)。
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum Profile {
18    /// 标准调用:跨方言对齐的公共能力集;DB 独有能力可用但须注释标注(不建议业务查询)
19    #[default]
20    Standard,
21    /// AI 问数:功能收缩 + 硬性限制
22    Text2Query,
23}
24
25impl Profile {
26    /// 档位字符串(Host / 绑定层单点取用)
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            Profile::Standard => "standard",
30            Profile::Text2Query => "text2query",
31        }
32    }
33
34    /// 字符串 → 档位;未知值 **Err**(禁静默回落 `Standard`,见执行文档 §7)
35    pub fn from_str_or_err(s: &str) -> Result<Profile, String> {
36        match s {
37            "standard" => Ok(Profile::Standard),
38            "text2query" => Ok(Profile::Text2Query),
39            other => Err(format!("未知 profile: {other}(仅 standard / text2query)")),
40        }
41    }
42}
43
44#[derive(Debug, Clone, Default)]
45pub struct Registry {
46    schemas: HashMap<String, Schema>,
47    order: Vec<String>,
48    /// 上下文强制开关(默认关闭 = fail-open,与 JS 原版 parity);
49    /// 开启后所有 plan 入口对 `ctx: None` 显式报错(fail-secure,见 `permission` 模块文档)。
50    /// 内部调用请传显式系统上下文(JSON `{"internal": true}` / `Context::system()`)。
51    require_context: bool,
52    /// 查询档位(默认 [`Profile::Standard`];text2query 由 AI 问数链路显式进入)。
53    profile: Profile,
54    /// RBAC 动态策略(`None` = 未启用,判决原语直通、行为与现状一致)。见 [`crate::rbac`]
55    rbac: Option<crate::rbac::RbacPolicy>,
56    /// 角色清单与未配置姿态(豁免 / 拒写 / Open|Closed,默认 []/[]/Open)。见 [`crate::permission::RoleRules`]
57    role_rules: crate::permission::RoleRules,
58    /// 定义层门禁策略(默认 Open —— 全放行,保持既有 parity)。见 [`crate::permission::MetaPolicy`]
59    meta_policy: crate::permission::MetaPolicy,
60}
61
62impl Registry {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    /// 注册一个 schema(含自动注册 `<Name>Deleted` 归档表),对应 JS `register`。
68    ///
69    /// 兼容入口:定义层门禁按当前 [`MetaPolicy`](crate::permission::MetaPolicy) 判决
70    /// (默认 Open → 全放行)。需带 ctx 显式过门禁请用 [`Self::register_with_ctx`]。
71    pub fn register(&mut self, defn: &Value) -> Result<(), String> {
72        self.register_with_ctx(defn, None)
73    }
74
75    /// 带 ctx 的注册:定义层门禁([`can_register`](crate::permission::can_register))
76    /// 判决**先于** `build_schema`(拒绝即返回,零副作用)。
77    ///
78    /// 归档附表 `<Name>Deleted` 的自动派生属内部动作,以系统上下文注册,不受业务 ctx 影响。
79    pub fn register_with_ctx(
80        &mut self,
81        defn: &Value,
82        ctx: Option<&crate::permission::Context>,
83    ) -> Result<(), String> {
84        let obj = defn
85            .as_object()
86            .ok_or_else(|| "schema 定义必须是对象".to_string())?;
87        let name = obj
88            .get("name")
89            .and_then(|v| v.as_str())
90            .ok_or_else(|| "schema 缺少 name".to_string())?
91            .to_string();
92
93        // 定义层门禁:判决先于 build_schema —— 拒绝即返回,绝不部分写入
94        if !crate::permission::can_register(&self.meta_policy, ctx) {
95            return Err(format!("ERR_PERMISSION: 无权注册或覆盖定义 {name}"));
96        }
97
98        let collection = obj
99            .get("collection")
100            .and_then(|v| v.as_str())
101            .filter(|s| !s.is_empty())
102            .map(String::from)
103            .unwrap_or_else(|| name.clone());
104
105        let schema = build_schema(obj, name.clone(), collection.clone())?;
106
107        // 定位三元组唯一性(fail fast,绝不静默串源):同名覆盖除外
108        self.check_location_unique(&name, &schema)?;
109
110        let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
111        self.schemas.insert(name.clone(), schema);
112        self.order.push(name.clone());
113
114        // 自动注册删除附表 schema —— 每个业务表对应一个 `<collection>_deleted` 归档表
115        // (内部动作,以系统上下文注册,不受业务门禁影响)
116        if !is_archive && !name.ends_with("Deleted") {
117            self.register_with_ctx(
118                &archive_defn(obj, &name, &collection),
119                Some(&crate::permission::Context::system()),
120            )?;
121        }
122
123        Ok(())
124    }
125
126    /// 按名称获取 schema
127    pub fn get(&self, name: &str) -> Result<&Schema, String> {
128        self.schemas
129            .get(name)
130            .ok_or_else(|| format!("Schema 未注册: {}", name))
131    }
132
133    pub fn has(&self, name: &str) -> bool {
134        self.schemas.contains_key(name)
135    }
136
137    /// 清空 schema 注册表(`schemas` + `order`),对应绑定层的测试隔离 / 动态重建场景。
138    ///
139    /// 只清 schema,**不动**配置开关(`require_context` / `profile` / `rbac` /
140    /// `role_rules`)与回调表(`clear_fns` 对称:各清各的)——开关生命周期属
141    /// Registry 配置面,不随 schema 集合重建而丢。
142    pub fn clear(&mut self) {
143        self.schemas.clear();
144        self.order.clear();
145    }
146
147    /// 开关「上下文强制」(默认关闭 = fail-open,保持 JS parity)。
148    /// 开启后:plan 入口遇 `ctx: None` 报 `ERR_NO_CONTEXT`(fail-secure)。
149    /// 内部调用须显式传系统上下文(`{"internal": true}`)。
150    pub fn set_require_context(&mut self, require: bool) {
151        self.require_context = require;
152    }
153
154    /// 「上下文强制」开关当前值
155    pub fn require_context(&self) -> bool {
156        self.require_context
157    }
158
159    /// 设置查询档位(`standard` / `text2query`)。
160    ///
161    /// 档位为**单值状态**(非栈):由 Host 的 `text2query()` 上下文管理器负责
162    /// 进入时设档、退出时恢复(见 `py-store` / `nodejs-store` 门面)。判决一律在 core。
163    pub fn set_profile(&mut self, profile: Profile) {
164        self.profile = profile;
165    }
166
167    /// 当前查询档位
168    pub fn profile(&self) -> Profile {
169        self.profile
170    }
171
172    /// 豁免角色清单:命中者在一切判决环节(静态 + RBAC)直接放行。默认空——无豁免。
173    pub fn set_exempt_roles(&mut self, roles: Vec<String>) {
174        self.role_rules.exempt_roles = roles;
175    }
176
177    /// 拒写角色清单:命中者一切写路径拒绝(读不受影响)。默认空——无拒写。
178    pub fn set_deny_write_roles(&mut self, roles: Vec<String>) {
179        self.role_rules.deny_write_roles = roles;
180    }
181
182    /// schema 白名单缺失/为空时的默认姿态。默认 Open(保持现状语义)。
183    pub fn set_unconfigured_policy(&mut self, policy: crate::permission::UnconfiguredPolicy) {
184        self.role_rules.unconfigured = policy;
185    }
186
187    /// 当前角色规则(静态判决函数与 RBAC decide 的共用取参入口)
188    pub fn role_rules(&self) -> &crate::permission::RoleRules {
189        &self.role_rules
190    }
191
192    /// 定义层门禁策略(`closed=true` 时仅 internal 或 `roles` 白名单可注册/覆盖)。
193    /// 默认 Open —— 全放行(保持既有 parity;须在首次业务注册前调用方生效于该次注册)。
194    pub fn set_meta_policy(&mut self, closed: bool, roles: Vec<String>) {
195        self.meta_policy = crate::permission::MetaPolicy { closed, roles };
196    }
197
198    /// 当前定义层门禁策略
199    pub fn meta_policy(&self) -> &crate::permission::MetaPolicy {
200        &self.meta_policy
201    }
202
203    /// 只读定义层判决(`workflow` 等宿主侧定义面复用同一门禁;判决唯一在 core)。
204    ///
205    /// 不改变任何注册状态:Open 全放行;Closed 仅 `internal` 或 `roles` 白名单角色。
206    /// 语义与 [`Self::register_with_ctx`] 的门禁判据**完全一致**(同一
207    /// [`can_register`](crate::permission::can_register)),仅作只读暴露。
208    pub fn can_register(&self, ctx: Option<&crate::permission::Context>) -> bool {
209        crate::permission::can_register(&self.meta_policy, ctx)
210    }
211
212    /// 注入/清除 RBAC 动态策略;`None` = 关闭(判决原语直通)。
213    /// 解析失败 Err(fail-fast,禁静默吞配置错误),成功后判决链路即刻生效。
214    pub fn set_rbac(&mut self, policy: Option<&Value>) -> Result<(), String> {
215        match policy {
216            None => {
217                self.rbac = None;
218                Ok(())
219            }
220            Some(v) => {
221                let p = crate::rbac::RbacPolicy::from_json(v)?;
222                self.rbac = Some(p);
223                Ok(())
224            }
225        }
226    }
227
228    /// 当前 RBAC 策略(`None` = 未启用)
229    pub fn rbac(&self) -> Option<&crate::rbac::RbacPolicy> {
230        self.rbac.as_ref()
231    }
232
233    /// 按定位三元组精确获取 schema(命令路由的唯一定位入口)
234    ///
235    /// `(source, namespace, collection)` 三元组在 Registry 内唯一(注册期校验),
236    /// 故此处零候选 = 未注册;一候选 = 精确命中。**不做任何猜测回落**。
237    pub fn get_by_location(
238        &self,
239        source: &str,
240        namespace: Option<&str>,
241        collection: &str,
242    ) -> Result<&Schema, String> {
243        let ns = namespace.filter(|s| !s.is_empty());
244        self.schemas
245            .values()
246            .find(|s| s.collection == collection && s.source() == source && s.ns() == ns)
247            .ok_or_else(|| {
248                format!(
249                    "Schema 未定位(source = {}, namespace = {:?}, collection = {})",
250                    source, ns, collection
251                )
252            })
253    }
254
255    /// 命令结构定位(方言翻译用):优先三元组精确命中;未命中时(`route_override`
256    /// 改写 namespace 的多租户场景)回落 `(source, collection)` 定位 —— 结构 schema
257    /// 与定位 namespace 正交(见 multi-datasource-routing-plan.md §6:权限/字段
258    /// 校验仍按结构 schema)。
259    ///
260    /// `(source, collection)` 恰一候选 → 命中;多候选时取 `namespace = null` 的
261    /// 结构声明,无 null 声明 → 报错(拒绝猜测,铁律 3)。
262    pub fn get_for_command(
263        &self,
264        source: &str,
265        namespace: Option<&str>,
266        collection: &str,
267    ) -> Result<&Schema, String> {
268        if let Ok(s) = self.get_by_location(source, namespace, collection) {
269            return Ok(s);
270        }
271        let candidates: Vec<&Schema> = self
272            .schemas
273            .values()
274            .filter(|s| s.collection == collection && s.source() == source)
275            .collect();
276        match candidates.len() {
277            0 => self.get_by_location(source, namespace, collection),
278            1 => Ok(candidates[0]),
279            _ => candidates
280                .iter()
281                .find(|s| s.ns().is_none())
282                .copied()
283                .ok_or_else(|| {
284                    format!(
285                        "命令定位 ({}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
286                        source, namespace, collection
287                    )
288                }),
289        }
290    }
291
292    /// 定位三元组唯一性校验:不同 schema 名占用同一 `(source, namespace, collection)`
293    /// → Err(同名覆盖 = 更新语义,放行)。
294    fn check_location_unique(&self, name: &str, schema: &Schema) -> Result<(), String> {
295        let conflict = self.schemas.iter().find(|(n, s)| {
296            n.as_str() != name
297                && s.collection == schema.collection
298                && s.source() == schema.source()
299                && s.ns() == schema.ns()
300        });
301        match conflict {
302            None => Ok(()),
303            Some((other, s)) => Err(format!(
304                "定位三元组冲突: ({}, {:?}, {}) 已被 schema `{}`(collection = {})占用",
305                schema.source(),
306                schema.ns(),
307                schema.collection,
308                other,
309                s.collection
310            )),
311        }
312    }
313
314    pub fn list(&self) -> Vec<String> {
315        self.order.clone()
316    }
317
318    /// schema 声明的数据源名(未声明 → `None`,语义为 `default`)
319    pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
320        Ok(self.get(name)?.datasource.clone())
321    }
322
323    /// 解析 schema 绑定的数据源(`config` 为 `{ "sources": { name: kind } }`)
324    ///
325    /// 纯逻辑:不持有连接(连接句柄留在 Host)。`null` / 空配置下等价单源 Mongo,
326    /// 保证既有调用方与 parity fixture 零变更。见 [`crate::datasource`]。
327    pub fn resolve_datasource(
328        &self,
329        schema_name: &str,
330        config: &Value,
331    ) -> Result<crate::datasource::DataSource, String> {
332        let declared = self.get(schema_name)?.datasource.as_deref();
333        let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
334        cfg.resolve(declared)
335    }
336}
337
338/// 由原始定义构建 [`Schema`](字段/关系/计算列/元数据分块解析,保持单一职责)
339fn build_schema(
340    obj: &Map<String, Value>,
341    name: String,
342    collection: String,
343) -> Result<Schema, String> {
344    let timestamps = parse_timestamps(obj)?;
345    let mut fields = normalize_fields(obj.get("fields"))?;
346    if timestamps {
347        add_timestamp_fields(&mut fields);
348    }
349    let relations = parse_relations(obj);
350    // 计算列的 agg 需要校验「引用的关系存在」→ 必须晚于 relations 解析
351    let computes = parse_computes(obj, &relations)?;
352    Ok(Schema {
353        name,
354        collection,
355        id_prefix: obj
356            .get("idPrefix")
357            .and_then(|v| v.as_str())
358            .unwrap_or("")
359            .to_string(),
360        timestamps,
361        fields,
362        relations,
363        computes,
364        read: str_list(obj.get("read")),
365        write: str_list(obj.get("write")),
366        indexes: obj
367            .get("indexes")
368            .and_then(|v| v.as_array())
369            .cloned()
370            .unwrap_or_default(),
371        datasource: opt_nonempty_str(obj.get("datasource")),
372        namespace: opt_nonempty_str(obj.get("namespace")),
373    })
374}
375
376/// `timestamps` 仅接受 true/false/'ms'/'s';单位换算在 Host(core 无时钟,now 由 Host 传入)
377fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
378    match obj.get("timestamps") {
379        None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
380        Some(Value::Bool(false)) => Ok(false),
381        Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
382        Some(Value::String(s)) => Err(format!(
383            "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
384            s
385        )),
386        Some(other) => Err(format!(
387            "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
388            other
389        )),
390    }
391}
392
393/// 自动补时间戳字段(`timestamps !== false` 时;已声明则不覆盖)
394fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
395    for key in ["createdAt", "updatedAt"] {
396        fields.entry(key.to_string()).or_insert_with(|| FieldDef {
397            field_type: "number".to_string(),
398            required: false,
399            default: None,
400            read: None,
401            write: None,
402            fields: None,
403            strategy: None,
404        });
405    }
406}
407
408fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
409    let mut relations = HashMap::new();
410    if let Some(Value::Object(rm)) = obj.get("relations") {
411        for (key, val) in rm {
412            let vo = val.as_object();
413            let get_str = |k: &str| -> Option<String> {
414                vo.and_then(|o| o.get(k))
415                    .and_then(|v| v.as_str())
416                    .filter(|s| !s.is_empty())
417                    .map(String::from)
418            };
419            relations.insert(
420                key.clone(),
421                RelationDef {
422                    model: get_str("model").unwrap_or_default(),
423                    rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
424                    local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
425                    foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
426                    read: str_list(vo.and_then(|o| o.get("read"))),
427                },
428            );
429        }
430    }
431    relations
432}
433
434fn parse_computes(
435    obj: &Map<String, Value>,
436    relations: &HashMap<String, RelationDef>,
437) -> Result<Vec<(String, ComputeDef)>, String> {
438    let mut computes = Vec::new();
439    if let Some(Value::Object(cm)) = obj.get("computes") {
440        for (key, val) in cm {
441            let vo = val.as_object();
442            let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
443            let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
444            let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
445            let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
446            computes.push((
447                key.clone(),
448                ComputeDef {
449                    comp_type: get("type")
450                        .and_then(|v| v.as_str().map(String::from))
451                        .filter(|s| !s.is_empty())
452                        .unwrap_or_else(|| "any".to_string()),
453                    has_fn,
454                    has_async_fn,
455                    fn_ref: get("fnRef")
456                        .and_then(|v| v.as_str().map(String::from))
457                        .filter(|s| !s.is_empty()),
458                    agg,
459                    depends: str_list(get("depends").as_ref()).unwrap_or_default(),
460                    read: str_list(get("read").as_ref()),
461                },
462            ));
463        }
464    }
465    Ok(computes)
466}
467
468/// 解析并校验归一聚合计算列(§9.2(2)):`{"$count":"lessons"}` / `{"$sum":"lessons.duration"}`。
469///
470/// - 算子白名单 `$count/$sum/$avg/$min/$max`,不在白名单 → Err(绝不静默);
471/// - 取值必须是**关系路径**:`$count` 仅关系整行计数(`lessons`,不接受字段),
472///   其余算子必须带字段(`lessons.duration`);
473/// - 二级关系路径(`orders.items.price`)首批不支持 → Err;
474/// - 与 `fn` / `asyncFn` 互斥。
475fn parse_agg(
476    key: &str,
477    raw: Option<&Value>,
478    relations: &HashMap<String, RelationDef>,
479    has_callback: bool,
480) -> Result<Option<Value>, String> {
481    let Some(v) = raw.filter(|v| is_truthy(v)) else {
482        return Ok(None);
483    };
484    if has_callback {
485        return Err(format!(
486            "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
487        ));
488    }
489    let obj = v.as_object().ok_or_else(|| {
490        format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
491    })?;
492    let mut it = obj.iter();
493    let (op, arg) = match (it.next(), it.next()) {
494        (Some(kv), None) => kv,
495        _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
496    };
497    if !AGG_OPS.contains(&op.as_str()) {
498        return Err(format!(
499            "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
500        ));
501    }
502    let path = arg
503        .as_str()
504        .filter(|s| !s.is_empty() && *s != "*")
505        .ok_or_else(|| {
506            format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
507        })?;
508    let (rel, field) = match path.split_once('.') {
509        Some((r, f)) => (r, Some(f)),
510        None => (path, None),
511    };
512    if !relations.contains_key(rel) {
513        return Err(format!(
514            "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
515        ));
516    }
517    if field
518        .map(|f| f.is_empty() || f.contains('.'))
519        .unwrap_or(false)
520    {
521        return Err(format!(
522            "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
523        ));
524    }
525    match op.as_str() {
526        "$count" if field.is_some() => Err(format!(
527            "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
528        )),
529        "$count" => Ok(Some(v.clone())),
530        _ if field.is_none() => Err(format!(
531            "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
532        )),
533        _ => Ok(Some(v.clone())),
534    }
535}
536
537/// 取非空字符串字段(空串归一为 `None`)
538fn opt_nonempty_str(v: Option<&Value>) -> Option<String> {
539    v.and_then(|v| v.as_str())
540        .filter(|s| !s.is_empty())
541        .map(String::from)
542}
543
544/// 构造 `<Name>Deleted` 归档表 schema 定义(字段 = 原字段 + `deletedAt`);
545/// 归档表与原表同库 —— `datasource` 与 `namespace` 一并继承。
546fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
547    let mut arch_fields = obj
548        .get("fields")
549        .and_then(|v| v.as_object())
550        .cloned()
551        .unwrap_or_default();
552    arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
553    // 剔除 `_id` 的自增策略:归档表显式拷贝源行 `_id` 值(upsert-by-_id 幂等),
554    // 不走数据库自增;DDL 侧也据此保持归档表 `_id` 为普通主键列
555    if let Some(idf) = arch_fields.get_mut("_id") {
556        if let Some(o) = idf.as_object_mut() {
557            o.remove("strategy");
558        }
559    }
560    let mut arch = json!({
561        "name": format!("{}Deleted", name),
562        "collection": format!("{}_deleted", collection),
563        "idPrefix": "",
564        "_isArchive": true,
565        "fields": Value::Object(arch_fields),
566        "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
567    });
568    if let Some(ds) = obj.get("datasource") {
569        arch["datasource"] = ds.clone();
570    }
571    if let Some(ns) = obj.get("namespace") {
572        arch["namespace"] = ns.clone();
573    }
574    arch
575}