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.get("_id").and_then(|f| f.strategy.as_deref()) == Some("autoincrement")
92    }
93}
94
95/// 规范化 fields 定义(字符串简写 → `{type, required:false}`)
96pub(super) fn normalize_fields(v: Option<&Value>) -> Result<HashMap<String, FieldDef>, String> {
97    let mut out = HashMap::new();
98    let Some(Value::Object(map)) = v else {
99        return Ok(out);
100    };
101    for (key, val) in map {
102        let fd = if let Some(s) = val.as_str() {
103            FieldDef {
104                field_type: s.to_string(),
105                required: false,
106                default: None,
107                read: None,
108                write: None,
109                fields: None,
110                strategy: None,
111            }
112        } else if let Some(o) = val.as_object() {
113            FieldDef {
114                field_type: o
115                    .get("type")
116                    .and_then(|v| v.as_str())
117                    .unwrap_or("")
118                    .to_string(),
119                required: o.get("required").and_then(|v| v.as_bool()).unwrap_or(false),
120                default: o.get("default").filter(|v| !v.is_null()).cloned(),
121                read: str_list(o.get("read")),
122                write: str_list(o.get("write")),
123                fields: o.get("fields").filter(|v| is_truthy(v)).cloned(),
124                strategy: match o.get("strategy").and_then(|v| v.as_str()) {
125                    None => None,
126                    Some(v @ "autoincrement") => Some(v.to_string()),
127                    // no-error-masking:未知策略显式拒绝,禁止静默吞掉后按默认行为跑
128                    Some(other) => {
129                        return Err(format!(
130                            "字段 {key} 的 strategy \"{other}\" 非法(仅支持 \"autoincrement\",且仅限 _id 字段)"
131                        ))
132                    }
133                },
134            }
135        } else {
136            // 非字符串、非对象的定义(如 `fields: { price: 123 }`)= 脏 schema,
137            // fail-fast 报错而非静默产出空 field_type(后续 object/array 展平、
138            // 标量列判定都会走错且无信号)
139            return Err(format!("字段 {} 定义类型非法(须为类型字符串或对象)", key));
140        };
141        out.insert(key.clone(), fd);
142    }
143    Ok(out)
144}
145
146/// 便捷构造:`Map` from `[(&str, Value)]`
147pub fn map_of(pairs: Vec<(&str, Value)>) -> Map<String, Value> {
148    let mut m = Map::new();
149    for (k, v) in pairs {
150        m.insert(k.to_string(), v);
151    }
152    m
153}