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 rbac: Option<crate::rbac::RbacPolicy>,
56 role_rules: crate::permission::RoleRules,
58 meta_policy: crate::permission::MetaPolicy,
60}
61
62impl Registry {
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn register(&mut self, defn: &Value) -> Result<(), String> {
72 self.register_with_ctx(defn, None)
73 }
74
75 pub fn register_with_ctx(
80 &mut self,
81 defn: &Value,
82 ctx: Option<&crate::permission::Context>,
83 ) -> Result<(), String> {
84 let obj = defn
85 .as_object()
86 .ok_or_else(|| "schema 定义必须是对象".to_string())?;
87 let name = obj
88 .get("name")
89 .and_then(|v| v.as_str())
90 .ok_or_else(|| "schema 缺少 name".to_string())?
91 .to_string();
92
93 if !crate::permission::can_register(&self.meta_policy, ctx) {
95 return Err(format!("ERR_PERMISSION: 无权注册或覆盖定义 {name}"));
96 }
97
98 let collection = obj
99 .get("collection")
100 .and_then(|v| v.as_str())
101 .filter(|s| !s.is_empty())
102 .map(String::from)
103 .unwrap_or_else(|| name.clone());
104
105 let schema = build_schema(obj, name.clone(), collection.clone())?;
106
107 self.check_location_unique(&name, &schema)?;
109
110 let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
111 self.schemas.insert(name.clone(), schema);
112 self.order.push(name.clone());
113
114 if !is_archive && !name.ends_with("Deleted") {
117 self.register_with_ctx(
118 &archive_defn(obj, &name, &collection),
119 Some(&crate::permission::Context::system()),
120 )?;
121 }
122
123 Ok(())
124 }
125
126 pub fn get(&self, name: &str) -> Result<&Schema, String> {
128 self.schemas
129 .get(name)
130 .ok_or_else(|| format!("Schema 未注册: {}", name))
131 }
132
133 pub fn has(&self, name: &str) -> bool {
134 self.schemas.contains_key(name)
135 }
136
137 pub fn clear(&mut self) {
143 self.schemas.clear();
144 self.order.clear();
145 }
146
147 pub fn set_require_context(&mut self, require: bool) {
151 self.require_context = require;
152 }
153
154 pub fn require_context(&self) -> bool {
156 self.require_context
157 }
158
159 pub fn set_profile(&mut self, profile: Profile) {
164 self.profile = profile;
165 }
166
167 pub fn profile(&self) -> Profile {
169 self.profile
170 }
171
172 pub fn set_exempt_roles(&mut self, roles: Vec<String>) {
174 self.role_rules.exempt_roles = roles;
175 }
176
177 pub fn set_deny_write_roles(&mut self, roles: Vec<String>) {
179 self.role_rules.deny_write_roles = roles;
180 }
181
182 pub fn set_unconfigured_policy(&mut self, policy: crate::permission::UnconfiguredPolicy) {
184 self.role_rules.unconfigured = policy;
185 }
186
187 pub fn role_rules(&self) -> &crate::permission::RoleRules {
189 &self.role_rules
190 }
191
192 pub fn set_meta_policy(&mut self, closed: bool, roles: Vec<String>) {
195 self.meta_policy = crate::permission::MetaPolicy { closed, roles };
196 }
197
198 pub fn meta_policy(&self) -> &crate::permission::MetaPolicy {
200 &self.meta_policy
201 }
202
203 pub fn set_rbac(&mut self, policy: Option<&Value>) -> Result<(), String> {
206 match policy {
207 None => {
208 self.rbac = None;
209 Ok(())
210 }
211 Some(v) => {
212 let p = crate::rbac::RbacPolicy::from_json(v)?;
213 self.rbac = Some(p);
214 Ok(())
215 }
216 }
217 }
218
219 pub fn rbac(&self) -> Option<&crate::rbac::RbacPolicy> {
221 self.rbac.as_ref()
222 }
223
224 pub fn get_by_location(
229 &self,
230 source: &str,
231 namespace: Option<&str>,
232 collection: &str,
233 ) -> Result<&Schema, String> {
234 let ns = namespace.filter(|s| !s.is_empty());
235 self.schemas
236 .values()
237 .find(|s| s.collection == collection && s.source() == source && s.ns() == ns)
238 .ok_or_else(|| {
239 format!(
240 "Schema 未定位(source = {}, namespace = {:?}, collection = {})",
241 source, ns, collection
242 )
243 })
244 }
245
246 pub fn get_for_command(
254 &self,
255 source: &str,
256 namespace: Option<&str>,
257 collection: &str,
258 ) -> Result<&Schema, String> {
259 if let Ok(s) = self.get_by_location(source, namespace, collection) {
260 return Ok(s);
261 }
262 let candidates: Vec<&Schema> = self
263 .schemas
264 .values()
265 .filter(|s| s.collection == collection && s.source() == source)
266 .collect();
267 match candidates.len() {
268 0 => self.get_by_location(source, namespace, collection),
269 1 => Ok(candidates[0]),
270 _ => candidates
271 .iter()
272 .find(|s| s.ns().is_none())
273 .copied()
274 .ok_or_else(|| {
275 format!(
276 "命令定位 ({}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
277 source, namespace, collection
278 )
279 }),
280 }
281 }
282
283 fn check_location_unique(&self, name: &str, schema: &Schema) -> Result<(), String> {
286 let conflict = self.schemas.iter().find(|(n, s)| {
287 n.as_str() != name
288 && s.collection == schema.collection
289 && s.source() == schema.source()
290 && s.ns() == schema.ns()
291 });
292 match conflict {
293 None => Ok(()),
294 Some((other, s)) => Err(format!(
295 "定位三元组冲突: ({}, {:?}, {}) 已被 schema `{}`(collection = {})占用",
296 schema.source(),
297 schema.ns(),
298 schema.collection,
299 other,
300 s.collection
301 )),
302 }
303 }
304
305 pub fn list(&self) -> Vec<String> {
306 self.order.clone()
307 }
308
309 pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
311 Ok(self.get(name)?.datasource.clone())
312 }
313
314 pub fn resolve_datasource(
319 &self,
320 schema_name: &str,
321 config: &Value,
322 ) -> Result<crate::datasource::DataSource, String> {
323 let declared = self.get(schema_name)?.datasource.as_deref();
324 let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
325 cfg.resolve(declared)
326 }
327}
328
329fn build_schema(
331 obj: &Map<String, Value>,
332 name: String,
333 collection: String,
334) -> Result<Schema, String> {
335 let timestamps = parse_timestamps(obj)?;
336 let mut fields = normalize_fields(obj.get("fields"))?;
337 if timestamps {
338 add_timestamp_fields(&mut fields);
339 }
340 let relations = parse_relations(obj);
341 let computes = parse_computes(obj, &relations)?;
343 Ok(Schema {
344 name,
345 collection,
346 id_prefix: obj
347 .get("idPrefix")
348 .and_then(|v| v.as_str())
349 .unwrap_or("")
350 .to_string(),
351 timestamps,
352 fields,
353 relations,
354 computes,
355 read: str_list(obj.get("read")),
356 write: str_list(obj.get("write")),
357 indexes: obj
358 .get("indexes")
359 .and_then(|v| v.as_array())
360 .cloned()
361 .unwrap_or_default(),
362 datasource: opt_nonempty_str(obj.get("datasource")),
363 namespace: opt_nonempty_str(obj.get("namespace")),
364 })
365}
366
367fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
369 match obj.get("timestamps") {
370 None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
371 Some(Value::Bool(false)) => Ok(false),
372 Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
373 Some(Value::String(s)) => Err(format!(
374 "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
375 s
376 )),
377 Some(other) => Err(format!(
378 "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
379 other
380 )),
381 }
382}
383
384fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
386 for key in ["createdAt", "updatedAt"] {
387 fields.entry(key.to_string()).or_insert_with(|| FieldDef {
388 field_type: "number".to_string(),
389 required: false,
390 default: None,
391 read: None,
392 write: None,
393 fields: None,
394 strategy: None,
395 });
396 }
397}
398
399fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
400 let mut relations = HashMap::new();
401 if let Some(Value::Object(rm)) = obj.get("relations") {
402 for (key, val) in rm {
403 let vo = val.as_object();
404 let get_str = |k: &str| -> Option<String> {
405 vo.and_then(|o| o.get(k))
406 .and_then(|v| v.as_str())
407 .filter(|s| !s.is_empty())
408 .map(String::from)
409 };
410 relations.insert(
411 key.clone(),
412 RelationDef {
413 model: get_str("model").unwrap_or_default(),
414 rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
415 local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
416 foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
417 read: str_list(vo.and_then(|o| o.get("read"))),
418 },
419 );
420 }
421 }
422 relations
423}
424
425fn parse_computes(
426 obj: &Map<String, Value>,
427 relations: &HashMap<String, RelationDef>,
428) -> Result<Vec<(String, ComputeDef)>, String> {
429 let mut computes = Vec::new();
430 if let Some(Value::Object(cm)) = obj.get("computes") {
431 for (key, val) in cm {
432 let vo = val.as_object();
433 let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
434 let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
435 let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
436 let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
437 computes.push((
438 key.clone(),
439 ComputeDef {
440 comp_type: get("type")
441 .and_then(|v| v.as_str().map(String::from))
442 .filter(|s| !s.is_empty())
443 .unwrap_or_else(|| "any".to_string()),
444 has_fn,
445 has_async_fn,
446 fn_ref: get("fnRef")
447 .and_then(|v| v.as_str().map(String::from))
448 .filter(|s| !s.is_empty()),
449 agg,
450 depends: str_list(get("depends").as_ref()).unwrap_or_default(),
451 read: str_list(get("read").as_ref()),
452 },
453 ));
454 }
455 }
456 Ok(computes)
457}
458
459fn parse_agg(
467 key: &str,
468 raw: Option<&Value>,
469 relations: &HashMap<String, RelationDef>,
470 has_callback: bool,
471) -> Result<Option<Value>, String> {
472 let Some(v) = raw.filter(|v| is_truthy(v)) else {
473 return Ok(None);
474 };
475 if has_callback {
476 return Err(format!(
477 "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
478 ));
479 }
480 let obj = v.as_object().ok_or_else(|| {
481 format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
482 })?;
483 let mut it = obj.iter();
484 let (op, arg) = match (it.next(), it.next()) {
485 (Some(kv), None) => kv,
486 _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
487 };
488 if !AGG_OPS.contains(&op.as_str()) {
489 return Err(format!(
490 "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
491 ));
492 }
493 let path = arg
494 .as_str()
495 .filter(|s| !s.is_empty() && *s != "*")
496 .ok_or_else(|| {
497 format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
498 })?;
499 let (rel, field) = match path.split_once('.') {
500 Some((r, f)) => (r, Some(f)),
501 None => (path, None),
502 };
503 if !relations.contains_key(rel) {
504 return Err(format!(
505 "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
506 ));
507 }
508 if field
509 .map(|f| f.is_empty() || f.contains('.'))
510 .unwrap_or(false)
511 {
512 return Err(format!(
513 "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
514 ));
515 }
516 match op.as_str() {
517 "$count" if field.is_some() => Err(format!(
518 "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
519 )),
520 "$count" => Ok(Some(v.clone())),
521 _ if field.is_none() => Err(format!(
522 "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
523 )),
524 _ => Ok(Some(v.clone())),
525 }
526}
527
528fn opt_nonempty_str(v: Option<&Value>) -> Option<String> {
530 v.and_then(|v| v.as_str())
531 .filter(|s| !s.is_empty())
532 .map(String::from)
533}
534
535fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
538 let mut arch_fields = obj
539 .get("fields")
540 .and_then(|v| v.as_object())
541 .cloned()
542 .unwrap_or_default();
543 arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
544 if let Some(idf) = arch_fields.get_mut("_id") {
547 if let Some(o) = idf.as_object_mut() {
548 o.remove("strategy");
549 }
550 }
551 let mut arch = json!({
552 "name": format!("{}Deleted", name),
553 "collection": format!("{}_deleted", collection),
554 "idPrefix": "",
555 "_isArchive": true,
556 "fields": Value::Object(arch_fields),
557 "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
558 });
559 if let Some(ds) = obj.get("datasource") {
560 arch["datasource"] = ds.clone();
561 }
562 if let Some(ns) = obj.get("namespace") {
563 arch["namespace"] = ns.clone();
564 }
565 arch
566}