1use 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 require_context: bool,
19}
20
21impl Registry {
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 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 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 if !is_archive && !name.ends_with("Deleted") {
54 self.register(&archive_defn(obj, &name, &collection))?;
55 }
56
57 Ok(())
58 }
59
60 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 pub fn set_require_context(&mut self, require: bool) {
75 self.require_context = require;
76 }
77
78 pub fn require_context(&self) -> bool {
80 self.require_context
81 }
82
83 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 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 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 pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
170 Ok(self.get(name)?.datasource.clone())
171 }
172
173 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
188fn 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 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
226fn 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
243fn 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
317fn 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
386fn 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
393fn 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}