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, Copy, PartialEq, Eq, Default)]
17pub enum Profile {
18 #[default]
20 Standard,
21 Text2Query,
23}
24
25impl Profile {
26 pub fn as_str(&self) -> &'static str {
28 match self {
29 Profile::Standard => "standard",
30 Profile::Text2Query => "text2query",
31 }
32 }
33
34 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 require_context: bool,
52 profile: Profile,
54}
55
56impl Registry {
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn register(&mut self, defn: &Value) -> Result<(), String> {
63 let obj = defn
64 .as_object()
65 .ok_or_else(|| "schema 定义必须是对象".to_string())?;
66 let name = obj
67 .get("name")
68 .and_then(|v| v.as_str())
69 .ok_or_else(|| "schema 缺少 name".to_string())?
70 .to_string();
71 let collection = obj
72 .get("collection")
73 .and_then(|v| v.as_str())
74 .filter(|s| !s.is_empty())
75 .map(String::from)
76 .unwrap_or_else(|| name.clone());
77
78 let schema = build_schema(obj, name.clone(), collection.clone())?;
79
80 self.check_location_unique(&name, &schema)?;
82
83 let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
84 self.schemas.insert(name.clone(), schema);
85 self.order.push(name.clone());
86
87 if !is_archive && !name.ends_with("Deleted") {
89 self.register(&archive_defn(obj, &name, &collection))?;
90 }
91
92 Ok(())
93 }
94
95 pub fn get(&self, name: &str) -> Result<&Schema, String> {
97 self.schemas
98 .get(name)
99 .ok_or_else(|| format!("Schema 未注册: {}", name))
100 }
101
102 pub fn has(&self, name: &str) -> bool {
103 self.schemas.contains_key(name)
104 }
105
106 pub fn set_require_context(&mut self, require: bool) {
110 self.require_context = require;
111 }
112
113 pub fn require_context(&self) -> bool {
115 self.require_context
116 }
117
118 pub fn set_profile(&mut self, profile: Profile) {
123 self.profile = profile;
124 }
125
126 pub fn profile(&self) -> Profile {
128 self.profile
129 }
130
131 pub fn get_by_location(
136 &self,
137 source: &str,
138 namespace: Option<&str>,
139 collection: &str,
140 ) -> Result<&Schema, String> {
141 let ns = namespace.filter(|s| !s.is_empty());
142 self.schemas
143 .values()
144 .find(|s| s.collection == collection && s.source() == source && s.ns() == ns)
145 .ok_or_else(|| {
146 format!(
147 "Schema 未定位(source = {}, namespace = {:?}, collection = {})",
148 source, ns, collection
149 )
150 })
151 }
152
153 pub fn get_for_command(
161 &self,
162 source: &str,
163 namespace: Option<&str>,
164 collection: &str,
165 ) -> Result<&Schema, String> {
166 if let Ok(s) = self.get_by_location(source, namespace, collection) {
167 return Ok(s);
168 }
169 let candidates: Vec<&Schema> = self
170 .schemas
171 .values()
172 .filter(|s| s.collection == collection && s.source() == source)
173 .collect();
174 match candidates.len() {
175 0 => self.get_by_location(source, namespace, collection),
176 1 => Ok(candidates[0]),
177 _ => candidates
178 .iter()
179 .find(|s| s.ns().is_none())
180 .copied()
181 .ok_or_else(|| {
182 format!(
183 "命令定位 ({}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
184 source, namespace, collection
185 )
186 }),
187 }
188 }
189
190 fn check_location_unique(&self, name: &str, schema: &Schema) -> Result<(), String> {
193 let conflict = self.schemas.iter().find(|(n, s)| {
194 n.as_str() != name
195 && s.collection == schema.collection
196 && s.source() == schema.source()
197 && s.ns() == schema.ns()
198 });
199 match conflict {
200 None => Ok(()),
201 Some((other, s)) => Err(format!(
202 "定位三元组冲突: ({}, {:?}, {}) 已被 schema `{}`(collection = {})占用",
203 schema.source(),
204 schema.ns(),
205 schema.collection,
206 other,
207 s.collection
208 )),
209 }
210 }
211
212 pub fn list(&self) -> Vec<String> {
213 self.order.clone()
214 }
215
216 pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
218 Ok(self.get(name)?.datasource.clone())
219 }
220
221 pub fn resolve_datasource(
226 &self,
227 schema_name: &str,
228 config: &Value,
229 ) -> Result<crate::datasource::DataSource, String> {
230 let declared = self.get(schema_name)?.datasource.as_deref();
231 let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
232 cfg.resolve(declared)
233 }
234}
235
236fn build_schema(
238 obj: &Map<String, Value>,
239 name: String,
240 collection: String,
241) -> Result<Schema, String> {
242 let timestamps = parse_timestamps(obj)?;
243 let mut fields = normalize_fields(obj.get("fields"))?;
244 if timestamps {
245 add_timestamp_fields(&mut fields);
246 }
247 let relations = parse_relations(obj);
248 let computes = parse_computes(obj, &relations)?;
250 Ok(Schema {
251 name,
252 collection,
253 id_prefix: obj
254 .get("idPrefix")
255 .and_then(|v| v.as_str())
256 .unwrap_or("")
257 .to_string(),
258 timestamps,
259 fields,
260 relations,
261 computes,
262 read: str_list(obj.get("read")),
263 write: str_list(obj.get("write")),
264 indexes: obj
265 .get("indexes")
266 .and_then(|v| v.as_array())
267 .cloned()
268 .unwrap_or_default(),
269 datasource: opt_nonempty_str(obj.get("datasource")),
270 namespace: opt_nonempty_str(obj.get("namespace")),
271 })
272}
273
274fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
276 match obj.get("timestamps") {
277 None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
278 Some(Value::Bool(false)) => Ok(false),
279 Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
280 Some(Value::String(s)) => Err(format!(
281 "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
282 s
283 )),
284 Some(other) => Err(format!(
285 "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
286 other
287 )),
288 }
289}
290
291fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
293 for key in ["createdAt", "updatedAt"] {
294 fields.entry(key.to_string()).or_insert_with(|| FieldDef {
295 field_type: "number".to_string(),
296 required: false,
297 default: None,
298 read: None,
299 write: None,
300 fields: None,
301 strategy: None,
302 });
303 }
304}
305
306fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
307 let mut relations = HashMap::new();
308 if let Some(Value::Object(rm)) = obj.get("relations") {
309 for (key, val) in rm {
310 let vo = val.as_object();
311 let get_str = |k: &str| -> Option<String> {
312 vo.and_then(|o| o.get(k))
313 .and_then(|v| v.as_str())
314 .filter(|s| !s.is_empty())
315 .map(String::from)
316 };
317 relations.insert(
318 key.clone(),
319 RelationDef {
320 model: get_str("model").unwrap_or_default(),
321 rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
322 local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
323 foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
324 read: str_list(vo.and_then(|o| o.get("read"))),
325 },
326 );
327 }
328 }
329 relations
330}
331
332fn parse_computes(
333 obj: &Map<String, Value>,
334 relations: &HashMap<String, RelationDef>,
335) -> Result<Vec<(String, ComputeDef)>, String> {
336 let mut computes = Vec::new();
337 if let Some(Value::Object(cm)) = obj.get("computes") {
338 for (key, val) in cm {
339 let vo = val.as_object();
340 let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
341 let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
342 let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
343 let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
344 computes.push((
345 key.clone(),
346 ComputeDef {
347 comp_type: get("type")
348 .and_then(|v| v.as_str().map(String::from))
349 .filter(|s| !s.is_empty())
350 .unwrap_or_else(|| "any".to_string()),
351 has_fn,
352 has_async_fn,
353 fn_ref: get("fnRef")
354 .and_then(|v| v.as_str().map(String::from))
355 .filter(|s| !s.is_empty()),
356 agg,
357 depends: str_list(get("depends").as_ref()).unwrap_or_default(),
358 read: str_list(get("read").as_ref()),
359 },
360 ));
361 }
362 }
363 Ok(computes)
364}
365
366fn parse_agg(
374 key: &str,
375 raw: Option<&Value>,
376 relations: &HashMap<String, RelationDef>,
377 has_callback: bool,
378) -> Result<Option<Value>, String> {
379 let Some(v) = raw.filter(|v| is_truthy(v)) else {
380 return Ok(None);
381 };
382 if has_callback {
383 return Err(format!(
384 "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
385 ));
386 }
387 let obj = v.as_object().ok_or_else(|| {
388 format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
389 })?;
390 let mut it = obj.iter();
391 let (op, arg) = match (it.next(), it.next()) {
392 (Some(kv), None) => kv,
393 _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
394 };
395 if !AGG_OPS.contains(&op.as_str()) {
396 return Err(format!(
397 "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
398 ));
399 }
400 let path = arg
401 .as_str()
402 .filter(|s| !s.is_empty() && *s != "*")
403 .ok_or_else(|| {
404 format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
405 })?;
406 let (rel, field) = match path.split_once('.') {
407 Some((r, f)) => (r, Some(f)),
408 None => (path, None),
409 };
410 if !relations.contains_key(rel) {
411 return Err(format!(
412 "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
413 ));
414 }
415 if field
416 .map(|f| f.is_empty() || f.contains('.'))
417 .unwrap_or(false)
418 {
419 return Err(format!(
420 "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
421 ));
422 }
423 match op.as_str() {
424 "$count" if field.is_some() => Err(format!(
425 "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
426 )),
427 "$count" => Ok(Some(v.clone())),
428 _ if field.is_none() => Err(format!(
429 "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
430 )),
431 _ => Ok(Some(v.clone())),
432 }
433}
434
435fn opt_nonempty_str(v: Option<&Value>) -> Option<String> {
437 v.and_then(|v| v.as_str())
438 .filter(|s| !s.is_empty())
439 .map(String::from)
440}
441
442fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
445 let mut arch_fields = obj
446 .get("fields")
447 .and_then(|v| v.as_object())
448 .cloned()
449 .unwrap_or_default();
450 arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
451 if let Some(idf) = arch_fields.get_mut("_id") {
454 if let Some(o) = idf.as_object_mut() {
455 o.remove("strategy");
456 }
457 }
458 let mut arch = json!({
459 "name": format!("{}Deleted", name),
460 "collection": format!("{}_deleted", collection),
461 "idPrefix": "",
462 "_isArchive": true,
463 "fields": Value::Object(arch_fields),
464 "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
465 });
466 if let Some(ds) = obj.get("datasource") {
467 arch["datasource"] = ds.clone();
468 }
469 if let Some(ns) = obj.get("namespace") {
470 arch["namespace"] = ns.clone();
471 }
472 arch
473}