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#[derive(Debug, Clone, Default)]
12pub struct Registry {
13    schemas: HashMap<String, Schema>,
14    order: Vec<String>,
15    /// 上下文强制开关(默认关闭 = fail-open,与 JS 原版 parity);
16    /// 开启后所有 plan 入口对 `ctx: None` 显式报错(fail-secure,见 `permission` 模块文档)。
17    /// 内部调用请传显式系统上下文(JSON `{"internal": true}` / `Context::system()`)。
18    require_context: bool,
19}
20
21impl Registry {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// 注册一个 schema(含自动注册 `<Name>Deleted` 归档表),对应 JS `register`
27    pub fn register(&mut self, defn: &Value) -> Result<(), String> {
28        let obj = defn
29            .as_object()
30            .ok_or_else(|| "schema 定义必须是对象".to_string())?;
31        let name = obj
32            .get("name")
33            .and_then(|v| v.as_str())
34            .ok_or_else(|| "schema 缺少 name".to_string())?
35            .to_string();
36        let collection = obj
37            .get("collection")
38            .and_then(|v| v.as_str())
39            .filter(|s| !s.is_empty())
40            .map(String::from)
41            .unwrap_or_else(|| name.clone());
42
43        let schema = build_schema(obj, name.clone(), collection.clone())?;
44
45        // 定位三元组唯一性(fail fast,绝不静默串源):同名覆盖除外
46        self.check_location_unique(&name, &schema)?;
47
48        let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
49        self.schemas.insert(name.clone(), schema);
50        self.order.push(name.clone());
51
52        // 自动注册删除附表 schema —— 每个业务表对应一个 `<collection>_deleted` 归档表
53        if !is_archive && !name.ends_with("Deleted") {
54            self.register(&archive_defn(obj, &name, &collection))?;
55        }
56
57        Ok(())
58    }
59
60    /// 按名称获取 schema
61    pub fn get(&self, name: &str) -> Result<&Schema, String> {
62        self.schemas
63            .get(name)
64            .ok_or_else(|| format!("Schema 未注册: {}", name))
65    }
66
67    pub fn has(&self, name: &str) -> bool {
68        self.schemas.contains_key(name)
69    }
70
71    /// 开关「上下文强制」(默认关闭 = fail-open,保持 JS parity)。
72    /// 开启后:plan 入口遇 `ctx: None` 报 `ERR_NO_CONTEXT`(fail-secure)。
73    /// 内部调用须显式传系统上下文(`{"internal": true}`)。
74    pub fn set_require_context(&mut self, require: bool) {
75        self.require_context = require;
76    }
77
78    /// 「上下文强制」开关当前值
79    pub fn require_context(&self) -> bool {
80        self.require_context
81    }
82
83    /// 按定位三元组精确获取 schema(命令路由的唯一定位入口)
84    ///
85    /// `(source, namespace, collection)` 三元组在 Registry 内唯一(注册期校验),
86    /// 故此处零候选 = 未注册;一候选 = 精确命中。**不做任何猜测回落**。
87    pub fn get_by_location(
88        &self,
89        source: &str,
90        namespace: Option<&str>,
91        collection: &str,
92    ) -> Result<&Schema, String> {
93        let ns = namespace.filter(|s| !s.is_empty());
94        self.schemas
95            .values()
96            .find(|s| s.collection == collection && s.source() == source && s.ns() == ns)
97            .ok_or_else(|| {
98                format!(
99                    "Schema 未定位(source = {}, namespace = {:?}, collection = {})",
100                    source, ns, collection
101                )
102            })
103    }
104
105    /// 命令结构定位(方言翻译用):优先三元组精确命中;未命中时(`route_override`
106    /// 改写 namespace 的多租户场景)回落 `(source, collection)` 定位 —— 结构 schema
107    /// 与定位 namespace 正交(见 multi-datasource-routing-plan.md §6:权限/字段
108    /// 校验仍按结构 schema)。
109    ///
110    /// `(source, collection)` 恰一候选 → 命中;多候选时取 `namespace = null` 的
111    /// 结构声明,无 null 声明 → 报错(拒绝猜测,铁律 3)。
112    pub fn get_for_command(
113        &self,
114        source: &str,
115        namespace: Option<&str>,
116        collection: &str,
117    ) -> Result<&Schema, String> {
118        if let Ok(s) = self.get_by_location(source, namespace, collection) {
119            return Ok(s);
120        }
121        let candidates: Vec<&Schema> = self
122            .schemas
123            .values()
124            .filter(|s| s.collection == collection && s.source() == source)
125            .collect();
126        match candidates.len() {
127            0 => self.get_by_location(source, namespace, collection),
128            1 => Ok(candidates[0]),
129            _ => candidates
130                .iter()
131                .find(|s| s.ns().is_none())
132                .copied()
133                .ok_or_else(|| {
134                    format!(
135                        "命令定位 ({}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
136                        source, namespace, collection
137                    )
138                }),
139        }
140    }
141
142    /// 定位三元组唯一性校验:不同 schema 名占用同一 `(source, namespace, collection)`
143    /// → Err(同名覆盖 = 更新语义,放行)。
144    fn check_location_unique(&self, name: &str, schema: &Schema) -> Result<(), String> {
145        let conflict = self.schemas.iter().find(|(n, s)| {
146            n.as_str() != name
147                && s.collection == schema.collection
148                && s.source() == schema.source()
149                && s.ns() == schema.ns()
150        });
151        match conflict {
152            None => Ok(()),
153            Some((other, s)) => Err(format!(
154                "定位三元组冲突: ({}, {:?}, {}) 已被 schema `{}`(collection = {})占用",
155                schema.source(),
156                schema.ns(),
157                schema.collection,
158                other,
159                s.collection
160            )),
161        }
162    }
163
164    pub fn list(&self) -> Vec<String> {
165        self.order.clone()
166    }
167
168    /// schema 声明的数据源名(未声明 → `None`,语义为 `default`)
169    pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
170        Ok(self.get(name)?.datasource.clone())
171    }
172
173    /// 解析 schema 绑定的数据源(`config` 为 `{ "sources": { name: kind } }`)
174    ///
175    /// 纯逻辑:不持有连接(连接句柄留在 Host)。`null` / 空配置下等价单源 Mongo,
176    /// 保证既有调用方与 parity fixture 零变更。见 [`crate::datasource`]。
177    pub fn resolve_datasource(
178        &self,
179        schema_name: &str,
180        config: &Value,
181    ) -> Result<crate::datasource::DataSource, String> {
182        let declared = self.get(schema_name)?.datasource.as_deref();
183        let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
184        cfg.resolve(declared)
185    }
186}
187
188/// 由原始定义构建 [`Schema`](字段/关系/计算列/元数据分块解析,保持单一职责)
189fn build_schema(
190    obj: &Map<String, Value>,
191    name: String,
192    collection: String,
193) -> Result<Schema, String> {
194    let timestamps = parse_timestamps(obj)?;
195    let mut fields = normalize_fields(obj.get("fields"))?;
196    if timestamps {
197        add_timestamp_fields(&mut fields);
198    }
199    let relations = parse_relations(obj);
200    // 计算列的 agg 需要校验「引用的关系存在」→ 必须晚于 relations 解析
201    let computes = parse_computes(obj, &relations)?;
202    Ok(Schema {
203        name,
204        collection,
205        id_prefix: obj
206            .get("idPrefix")
207            .and_then(|v| v.as_str())
208            .unwrap_or("")
209            .to_string(),
210        timestamps,
211        fields,
212        relations,
213        computes,
214        read: str_list(obj.get("read")),
215        write: str_list(obj.get("write")),
216        indexes: obj
217            .get("indexes")
218            .and_then(|v| v.as_array())
219            .cloned()
220            .unwrap_or_default(),
221        datasource: opt_nonempty_str(obj.get("datasource")),
222        namespace: opt_nonempty_str(obj.get("namespace")),
223    })
224}
225
226/// `timestamps` 仅接受 true/false/'ms'/'s';单位换算在 Host(core 无时钟,now 由 Host 传入)
227fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
228    match obj.get("timestamps") {
229        None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
230        Some(Value::Bool(false)) => Ok(false),
231        Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
232        Some(Value::String(s)) => Err(format!(
233            "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
234            s
235        )),
236        Some(other) => Err(format!(
237            "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
238            other
239        )),
240    }
241}
242
243/// 自动补时间戳字段(`timestamps !== false` 时;已声明则不覆盖)
244fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
245    for key in ["createdAt", "updatedAt"] {
246        fields.entry(key.to_string()).or_insert_with(|| FieldDef {
247            field_type: "number".to_string(),
248            required: false,
249            default: None,
250            read: None,
251            write: None,
252            fields: None,
253        });
254    }
255}
256
257fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
258    let mut relations = HashMap::new();
259    if let Some(Value::Object(rm)) = obj.get("relations") {
260        for (key, val) in rm {
261            let vo = val.as_object();
262            let get_str = |k: &str| -> Option<String> {
263                vo.and_then(|o| o.get(k))
264                    .and_then(|v| v.as_str())
265                    .filter(|s| !s.is_empty())
266                    .map(String::from)
267            };
268            relations.insert(
269                key.clone(),
270                RelationDef {
271                    model: get_str("model").unwrap_or_default(),
272                    rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
273                    local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
274                    foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
275                    read: str_list(vo.and_then(|o| o.get("read"))),
276                },
277            );
278        }
279    }
280    relations
281}
282
283fn parse_computes(
284    obj: &Map<String, Value>,
285    relations: &HashMap<String, RelationDef>,
286) -> Result<Vec<(String, ComputeDef)>, String> {
287    let mut computes = Vec::new();
288    if let Some(Value::Object(cm)) = obj.get("computes") {
289        for (key, val) in cm {
290            let vo = val.as_object();
291            let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
292            let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
293            let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
294            let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
295            computes.push((
296                key.clone(),
297                ComputeDef {
298                    comp_type: get("type")
299                        .and_then(|v| v.as_str().map(String::from))
300                        .filter(|s| !s.is_empty())
301                        .unwrap_or_else(|| "any".to_string()),
302                    has_fn,
303                    has_async_fn,
304                    fn_ref: get("fnRef")
305                        .and_then(|v| v.as_str().map(String::from))
306                        .filter(|s| !s.is_empty()),
307                    agg,
308                    depends: str_list(get("depends").as_ref()).unwrap_or_default(),
309                    read: str_list(get("read").as_ref()),
310                },
311            ));
312        }
313    }
314    Ok(computes)
315}
316
317/// 解析并校验归一聚合计算列(§9.2(2)):`{"$count":"lessons"}` / `{"$sum":"lessons.duration"}`。
318///
319/// - 算子白名单 `$count/$sum/$avg/$min/$max`,不在白名单 → Err(绝不静默);
320/// - 取值必须是**关系路径**:`$count` 仅关系整行计数(`lessons`,不接受字段),
321///   其余算子必须带字段(`lessons.duration`);
322/// - 二级关系路径(`orders.items.price`)首批不支持 → Err;
323/// - 与 `fn` / `asyncFn` 互斥。
324fn parse_agg(
325    key: &str,
326    raw: Option<&Value>,
327    relations: &HashMap<String, RelationDef>,
328    has_callback: bool,
329) -> Result<Option<Value>, String> {
330    let Some(v) = raw.filter(|v| is_truthy(v)) else {
331        return Ok(None);
332    };
333    if has_callback {
334        return Err(format!(
335            "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
336        ));
337    }
338    let obj = v.as_object().ok_or_else(|| {
339        format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
340    })?;
341    let mut it = obj.iter();
342    let (op, arg) = match (it.next(), it.next()) {
343        (Some(kv), None) => kv,
344        _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
345    };
346    if !AGG_OPS.contains(&op.as_str()) {
347        return Err(format!(
348            "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
349        ));
350    }
351    let path = arg
352        .as_str()
353        .filter(|s| !s.is_empty() && *s != "*")
354        .ok_or_else(|| {
355            format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
356        })?;
357    let (rel, field) = match path.split_once('.') {
358        Some((r, f)) => (r, Some(f)),
359        None => (path, None),
360    };
361    if !relations.contains_key(rel) {
362        return Err(format!(
363            "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
364        ));
365    }
366    if field
367        .map(|f| f.is_empty() || f.contains('.'))
368        .unwrap_or(false)
369    {
370        return Err(format!(
371            "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
372        ));
373    }
374    match op.as_str() {
375        "$count" if field.is_some() => Err(format!(
376            "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
377        )),
378        "$count" => Ok(Some(v.clone())),
379        _ if field.is_none() => Err(format!(
380            "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
381        )),
382        _ => Ok(Some(v.clone())),
383    }
384}
385
386/// 取非空字符串字段(空串归一为 `None`)
387fn opt_nonempty_str(v: Option<&Value>) -> Option<String> {
388    v.and_then(|v| v.as_str())
389        .filter(|s| !s.is_empty())
390        .map(String::from)
391}
392
393/// 构造 `<Name>Deleted` 归档表 schema 定义(字段 = 原字段 + `deletedAt`);
394/// 归档表与原表同库 —— `datasource` 与 `namespace` 一并继承。
395fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
396    let mut arch_fields = obj
397        .get("fields")
398        .and_then(|v| v.as_object())
399        .cloned()
400        .unwrap_or_default();
401    arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
402    let mut arch = json!({
403        "name": format!("{}Deleted", name),
404        "collection": format!("{}_deleted", collection),
405        "idPrefix": "",
406        "_isArchive": true,
407        "fields": Value::Object(arch_fields),
408        "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
409    });
410    if let Some(ds) = obj.get("datasource") {
411        arch["datasource"] = ds.clone();
412    }
413    if let Some(ns) = obj.get("namespace") {
414        arch["namespace"] = ns.clone();
415    }
416    arch
417}