Skip to main content

rust_store_core/schema/
definition.rs

1//! Schema 的数据结构与字段规范化(`Registry` 见 [`super::registry`])。
2
3use std::collections::HashMap;
4
5use serde_json::{Map, Value};
6
7use crate::types::{is_truthy, str_list};
8
9#[derive(Debug, Clone)]
10pub struct FieldDef {
11    pub field_type: String,
12    pub required: bool,
13    pub default: Option<Value>,
14    pub read: Option<Vec<String>>,
15    pub write: Option<Vec<String>>,
16    /// 嵌套 object 字段的原始定义(未规范化,与 JS 保持一致)
17    pub fields: Option<Value>,
18    /// ID 供给策略(阶段2):仅 `_id` 字段有意义 —— `"autoincrement"` = 数据库自增列
19    /// 赋值(SQL 后端),宿主不生成、core 不注入 `_id`;Mongo 后端无自增语义,
20    /// 执行器对此显式报错(AUTOINCREMENT_NOT_SUPPORTED)。其余值非法(注册期校验)。
21    pub strategy: Option<String>,
22}
23
24#[derive(Debug, Clone)]
25pub struct RelationDef {
26    pub model: String,
27    pub rel_type: String,
28    pub local_field: String,
29    pub foreign_field: String,
30    pub read: Option<Vec<String>>,
31}
32
33#[derive(Debug, Clone)]
34pub struct ComputeDef {
35    pub comp_type: String,
36    pub has_fn: bool,
37    pub has_async_fn: bool,
38    /// 回调标识:Host 经 FnRegistry 把它绑定到实现;缺省 = 计算列 key 名
39    pub fn_ref: Option<String>,
40    /// 归一聚合算子(§9.2(2)):`{"$count": "lessons"}` / `{"$sum": "lessons.duration"}`。
41    /// 取代旧的 `lookup` + `addFields` Mongo 专用形态;SQL / Mongo 走同一语法(执行下推优先)。
42    pub agg: Option<Value>,
43    pub depends: Vec<String>,
44    pub read: Option<Vec<String>>,
45}
46
47#[derive(Debug, Clone)]
48pub struct Schema {
49    pub name: String,
50    pub collection: String,
51    pub id_prefix: String,
52    pub timestamps: bool,
53    pub fields: HashMap<String, FieldDef>,
54    pub relations: HashMap<String, RelationDef>,
55    /// 保持注册顺序:`build_agg_stages` 的输出顺序依赖它
56    pub computes: Vec<(String, ComputeDef)>,
57    pub read: Option<Vec<String>>,
58    pub write: Option<Vec<String>>,
59    /// 原始索引定义(`[{keys: {...}, options: {...}}]`),upsert 条件构建依赖 unique 索引
60    pub indexes: Vec<Value>,
61    /// 绑定的数据源名(可选;缺省 = `default`,回落单源 Mongo)。见 [`crate::datasource`]
62    pub datasource: Option<String>,
63    /// 连接内的库/schema 名(可选;缺省 = `null`,用连接自身默认:Mongo db 实例的库名、
64    /// PG 的 search_path、MySQL 的连接库、SQLite 的 main)。
65    /// 与 `datasource`/`collection` 构成定位三元组,见 [`super::Registry::get_by_location`]。
66    pub namespace: Option<String>,
67}
68
69impl Schema {
70    pub fn compute(&self, name: &str) -> Option<&ComputeDef> {
71        self.computes
72            .iter()
73            .find(|(k, _)| k == name)
74            .map(|(_, c)| c)
75    }
76
77    /// 解析后的数据源名(缺省 `default`)
78    pub fn source(&self) -> &str {
79        self.datasource
80            .as_deref()
81            .unwrap_or(crate::datasource::DEFAULT_SOURCE)
82    }
83
84    /// 解析后的 namespace(空串归一为 `None`)
85    pub fn ns(&self) -> Option<&str> {
86        self.namespace.as_deref().filter(|s| !s.is_empty())
87    }
88
89    /// `_id` 是否声明 `strategy: "autoincrement"`(阶段2:数据库自增主键)
90    pub fn id_is_autoincrement(&self) -> bool {
91        self.fields
92            .get("_id")
93            .and_then(|f| f.strategy.as_deref())
94            == Some("autoincrement")
95    }
96}
97
98/// 规范化 fields 定义(字符串简写 → `{type, required:false}`)
99pub(super) fn normalize_fields(v: Option<&Value>) -> Result<HashMap<String, FieldDef>, String> {
100    let mut out = HashMap::new();
101    let Some(Value::Object(map)) = v else {
102        return Ok(out);
103    };
104    for (key, val) in map {
105        let fd = if let Some(s) = val.as_str() {
106            FieldDef {
107                field_type: s.to_string(),
108                required: false,
109                default: None,
110                read: None,
111                write: None,
112                fields: None,
113                strategy: None,
114            }
115        } else if let Some(o) = val.as_object() {
116            FieldDef {
117                field_type: o
118                    .get("type")
119                    .and_then(|v| v.as_str())
120                    .unwrap_or("")
121                    .to_string(),
122                required: o.get("required").and_then(|v| v.as_bool()).unwrap_or(false),
123                default: o.get("default").filter(|v| !v.is_null()).cloned(),
124                read: str_list(o.get("read")),
125                write: str_list(o.get("write")),
126                fields: o.get("fields").filter(|v| is_truthy(v)).cloned(),
127                strategy: match o.get("strategy").and_then(|v| v.as_str()) {
128                    None => None,
129                    Some(v @ "autoincrement") => Some(v.to_string()),
130                    // no-error-masking:未知策略显式拒绝,禁止静默吞掉后按默认行为跑
131                    Some(other) => {
132                        return Err(format!(
133                            "字段 {key} 的 strategy \"{other}\" 非法(仅支持 \"autoincrement\",且仅限 _id 字段)"
134                        ))
135                    }
136                },
137            }
138        } else {
139            // 非字符串、非对象的定义(如 `fields: { price: 123 }`)= 脏 schema,
140            // fail-fast 报错而非静默产出空 field_type(后续 object/array 展平、
141            // 标量列判定都会走错且无信号)
142            return Err(format!("字段 {} 定义类型非法(须为类型字符串或对象)", key));
143        };
144        out.insert(key.clone(), fd);
145    }
146    Ok(out)
147}
148
149/// 便捷构造:`Map` from `[(&str, Value)]`
150pub fn map_of(pairs: Vec<(&str, Value)>) -> Map<String, Value> {
151    let mut m = Map::new();
152    for (k, v) in pairs {
153        m.insert(k.to_string(), v);
154    }
155    m
156}