1use std::collections::HashMap;
4use std::sync::Arc;
5
6use serde_json::{json, Map, Value};
7
8use crate::types::{is_truthy, str_list, AGG_OPS};
9
10use super::definition::{normalize_fields, ComputeDef, FieldDef, Location, RelationDef, Schema};
11
12use crate::command::WriteLinkPolicy;
13use crate::error::{CoreError, ERR_POLICY_VIEW_READONLY};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum Profile {
22 #[default]
24 Standard,
25 Text2Query,
27}
28
29impl Profile {
30 pub fn as_str(&self) -> &'static str {
32 match self {
33 Profile::Standard => "standard",
34 Profile::Text2Query => "text2query",
35 }
36 }
37
38 pub fn from_str_or_err(s: &str) -> Result<Profile, String> {
40 match s {
41 "standard" => Ok(Profile::Standard),
42 "text2query" => Ok(Profile::Text2Query),
43 other => Err(format!("未知 profile: {other}(仅 standard / text2query)")),
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50struct Entry {
51 schema: Schema,
52 links: Vec<Location>,
54 version: u64,
56}
57
58pub(super) struct BatchClass {
60 pub order: Vec<String>,
62 pub primaries: HashMap<String, (Value, Location, String)>,
64 pub replicas: HashMap<String, Vec<(Value, Location)>>,
66}
67
68pub(super) enum BatchError {
70 Parse(String),
72 PrimaryDuplicate { name: String, indices: Vec<usize> },
74 LocationConflict(Box<LocationConflict>),
76}
77
78pub(super) fn classify_batch(items: &[(Value, Location)]) -> Result<BatchClass, BatchError> {
84 let mut order: Vec<String> = Vec::new();
85 let mut primaries: HashMap<String, (Value, Location, String)> = HashMap::new();
86 let mut primary_idx: HashMap<String, usize> = HashMap::new();
87 let mut replicas: HashMap<String, Vec<(Value, Location)>> = HashMap::new();
88
89 for (idx, (defn, loc)) in items.iter().enumerate() {
90 let obj = defn
91 .as_object()
92 .ok_or_else(|| BatchError::Parse("schema 定义必须是对象".to_string()))?;
93 let name = obj
94 .get("name")
95 .and_then(|v| v.as_str())
96 .ok_or_else(|| BatchError::Parse("schema 缺少 name".to_string()))?
97 .to_string();
98 if !order.iter().any(|n| n == &name) {
99 order.push(name.clone());
100 }
101
102 if obj.get("replica").map(is_truthy).unwrap_or(false) {
103 replicas
104 .entry(name)
105 .or_default()
106 .push((defn.clone(), loc.clone()));
107 continue;
108 }
109
110 let collection = obj
111 .get("collection")
112 .and_then(|v| v.as_str())
113 .filter(|s| !s.is_empty())
114 .map(String::from)
115 .unwrap_or_else(|| name.clone());
116 if let Some(first) = primary_idx.get(&name) {
117 return Err(BatchError::PrimaryDuplicate {
119 name,
120 indices: vec![*first, idx],
121 });
122 }
123 primary_idx.insert(name.clone(), idx);
124 primaries.insert(name, (defn.clone(), loc.clone(), collection));
125 }
126
127 let mut quads: Vec<(String, String, Location)> = Vec::new();
129 for name in &order {
130 if let Some((_, loc, coll)) = primaries.get(name) {
131 quads.push((name.clone(), coll.clone(), loc.clone()));
132 }
133 if let Some(rs) = replicas.get(name) {
134 for (defn, loc) in rs {
135 let coll = defn
136 .get("name")
137 .and_then(|v| v.as_str())
138 .unwrap_or(name)
139 .to_string();
140 quads.push((name.clone(), coll, loc.clone()));
141 }
142 }
143 }
144 if let Some(c) = detect_location_conflict(&quads) {
145 return Err(BatchError::LocationConflict(Box::new(c)));
146 }
147
148 Ok(BatchClass {
149 order,
150 primaries,
151 replicas,
152 })
153}
154
155pub(super) struct LocationConflict {
157 pub collection: String,
158 pub prev: String,
159 pub name: String,
160 pub source: String,
161 pub database: Option<String>,
162 pub schema: Option<String>,
163}
164
165pub(super) fn detect_location_conflict(
169 entries: &[(String, String, Location)],
170) -> Option<LocationConflict> {
171 let mut seen: HashMap<(String, Option<String>, Option<String>, String), String> =
172 HashMap::new();
173 for (name, collection, loc) in entries {
174 let key = (
175 loc.source.clone(),
176 loc.database.clone(),
177 loc.schema.clone(),
178 collection.clone(),
179 );
180 if let Some(prev) = seen.get(&key) {
181 if prev != name {
182 return Some(LocationConflict {
183 collection: collection.clone(),
184 prev: prev.clone(),
185 name: name.clone(),
186 source: loc.source.clone(),
187 database: loc.database.clone(),
188 schema: loc.schema.clone(),
189 });
190 }
191 } else {
192 seen.insert(key, name.clone());
193 }
194 }
195 None
196}
197
198fn batch_err_to_msg(e: BatchError) -> String {
200 match e {
201 BatchError::Parse(m) => m,
202 BatchError::PrimaryDuplicate { name, .. } => format!(
203 "同名主定义重复: {}(同一装载批次内 name 必须唯一,从定义请用 replica: true)",
204 name
205 ),
206 BatchError::LocationConflict(c) => format!(
207 "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
208 c.source, c.database, c.schema, c.collection, c.prev
209 ),
210 }
211}
212
213#[derive(Debug, Clone, Default)]
215pub struct Catalog {
216 entries: HashMap<String, Entry>,
217 order: Vec<String>,
218}
219
220#[derive(Debug, Clone, Default)]
222pub struct PolicyBundle {
223 pub(crate) require_context: bool,
227 pub(crate) profile: Profile,
229 pub(crate) rbac: Option<crate::rbac::RbacPolicy>,
231 pub(crate) role_rules: crate::permission::RoleRules,
233 pub(crate) meta_policy: crate::permission::MetaPolicy,
235 pub(crate) write_link_policy: WriteLinkPolicy,
237}
238
239#[derive(Debug, Clone)]
244pub struct Registry {
245 catalog: Arc<Catalog>,
246 policy: PolicyBundle,
247 is_base: bool,
249}
250
251impl Default for Registry {
253 fn default() -> Self {
254 Self {
255 catalog: Arc::new(Catalog::default()),
256 policy: PolicyBundle::default(),
257 is_base: true,
258 }
259 }
260}
261
262#[derive(Debug, Clone, Default)]
265pub struct PolicyOverrides {
266 require_context: Option<bool>,
267 profile: Option<Profile>,
268 rbac: Option<Option<crate::rbac::RbacPolicy>>,
269 role_rules: Option<crate::permission::RoleRules>,
270 meta_policy: Option<crate::permission::MetaPolicy>,
271 write_link_policy: Option<WriteLinkPolicy>,
272}
273
274impl PolicyOverrides {
275 pub fn from_value(v: &Value) -> Result<Self, CoreError> {
281 let obj = v
282 .as_object()
283 .ok_or_else(|| CoreError::Other("策略覆盖必须是 JSON 对象".to_string()))?;
284 let mut o = PolicyOverrides::default();
285 for (k, val) in obj {
286 match k.as_str() {
287 "profile" => {
288 let s = val
289 .as_str()
290 .ok_or_else(|| CoreError::Other("profile 必须是字符串".to_string()))?;
291 o.profile = Some(Profile::from_str_or_err(s).map_err(CoreError::Other)?);
292 }
293 "requireContext" => {
294 o.require_context = Some(val.as_bool().ok_or_else(|| {
295 CoreError::Other("requireContext 必须是布尔".to_string())
296 })?);
297 }
298 "rbac" => {
299 o.rbac = Some(match val {
301 Value::Null => None,
302 other => Some(
303 crate::rbac::RbacPolicy::from_json(other).map_err(CoreError::Other)?,
304 ),
305 });
306 }
307 "roleRules" => {
308 o.role_rules = Some(parse_role_rules(val)?);
309 }
310 "metaPolicy" => {
311 o.meta_policy = Some(parse_meta_policy(val)?);
312 }
313 "writeLinkPolicy" => {
314 let s = val.as_str().ok_or_else(|| {
315 CoreError::Other("writeLinkPolicy 必须是字符串".to_string())
316 })?;
317 o.write_link_policy = Some(match s {
318 "reject" => WriteLinkPolicy::Reject,
319 "primaryOnly" => WriteLinkPolicy::PrimaryOnly,
320 other => {
321 return Err(CoreError::Other(format!(
322 "未知 writeLinkPolicy: {other}(仅 reject / primaryOnly)"
323 )))
324 }
325 });
326 }
327 other => {
328 return Err(CoreError::Other(format!(
329 "{ERR_POLICY_VIEW_READONLY} 未知策略覆盖键 `{other}`"
330 )))
331 }
332 }
333 }
334 Ok(o)
335 }
336}
337
338fn parse_role_rules(v: &Value) -> Result<crate::permission::RoleRules, CoreError> {
340 let obj = v
341 .as_object()
342 .ok_or_else(|| CoreError::Other("roleRules 必须是 JSON 对象".to_string()))?;
343 let mut rr = crate::permission::RoleRules::default();
344 for (k, val) in obj {
345 match k.as_str() {
346 "exemptRoles" => rr.exempt_roles = str_list_strict(val, "roleRules.exemptRoles")?,
347 "denyWriteRoles" => {
348 rr.deny_write_roles = str_list_strict(val, "roleRules.denyWriteRoles")?
349 }
350 "unconfigured" => {
351 let s = val.as_str().ok_or_else(|| {
352 CoreError::Other("roleRules.unconfigured 必须是字符串".to_string())
353 })?;
354 rr.unconfigured = crate::permission::UnconfiguredPolicy::from_str_or_err(s)
355 .map_err(CoreError::Other)?;
356 }
357 other => {
358 return Err(CoreError::Other(format!(
359 "{ERR_POLICY_VIEW_READONLY} 未知 roleRules 键 `{other}`"
360 )))
361 }
362 }
363 }
364 Ok(rr)
365}
366
367fn parse_meta_policy(v: &Value) -> Result<crate::permission::MetaPolicy, CoreError> {
369 let obj = v
370 .as_object()
371 .ok_or_else(|| CoreError::Other("metaPolicy 必须是 JSON 对象".to_string()))?;
372 let mut mp = crate::permission::MetaPolicy::default();
373 for (k, val) in obj {
374 match k.as_str() {
375 "closed" => {
376 mp.closed = val
377 .as_bool()
378 .ok_or_else(|| CoreError::Other("metaPolicy.closed 必须是布尔".to_string()))?;
379 }
380 "roles" => mp.roles = str_list_strict(val, "metaPolicy.roles")?,
381 other => {
382 return Err(CoreError::Other(format!(
383 "{ERR_POLICY_VIEW_READONLY} 未知 metaPolicy 键 `{other}`"
384 )))
385 }
386 }
387 }
388 Ok(mp)
389}
390
391fn str_list_strict(v: &Value, key: &str) -> Result<Vec<String>, CoreError> {
393 let arr = v
394 .as_array()
395 .ok_or_else(|| CoreError::Other(format!("{key} 必须是字符串数组")))?;
396 arr.iter()
397 .map(|x| {
398 x.as_str()
399 .map(String::from)
400 .ok_or_else(|| CoreError::Other(format!("{key} 的元素必须都是字符串")))
401 })
402 .collect()
403}
404
405impl PolicyBundle {
406 fn overridden(&self, o: &PolicyOverrides) -> PolicyBundle {
408 PolicyBundle {
409 require_context: o.require_context.unwrap_or(self.require_context),
410 profile: o.profile.unwrap_or(self.profile),
411 rbac: match &o.rbac {
412 Some(v) => v.clone(),
413 None => self.rbac.clone(),
414 },
415 role_rules: o
416 .role_rules
417 .clone()
418 .unwrap_or_else(|| self.role_rules.clone()),
419 meta_policy: o
420 .meta_policy
421 .clone()
422 .unwrap_or_else(|| self.meta_policy.clone()),
423 write_link_policy: o.write_link_policy.unwrap_or(self.write_link_policy),
424 }
425 }
426}
427
428impl Registry {
429 pub fn new() -> Self {
430 Self::default()
431 }
432
433 pub fn with_policy(&self, overrides: &PolicyOverrides) -> Registry {
438 Registry {
439 catalog: Arc::clone(&self.catalog),
440 policy: self.policy.overridden(overrides),
441 is_base: false,
442 }
443 }
444
445 pub fn is_view(&self) -> bool {
447 !self.is_base
448 }
449
450 fn ensure_base_catalog(&self) -> Result<(), String> {
456 if !self.is_base {
457 return Err(format!(
458 "{ERR_POLICY_VIEW_READONLY} 非 base 策略视图禁止注册/清空 schema(目录唯一真源在 base Registry)"
459 ));
460 }
461 Ok(())
462 }
463
464 pub fn register(&mut self, defn: &Value) -> Result<(), String> {
470 self.register_with_ctx(defn, None)
471 }
472
473 pub fn register_with_ctx(
475 &mut self,
476 defn: &Value,
477 ctx: Option<&crate::permission::Context>,
478 ) -> Result<(), String> {
479 self.register_batch(&[(defn.clone(), Location::default())], ctx)
480 }
481
482 pub fn register_batch(
497 &mut self,
498 items: &[(Value, Location)],
499 ctx: Option<&crate::permission::Context>,
500 ) -> Result<(), String> {
501 self.ensure_base_catalog()?; let class = classify_batch(items).map_err(batch_err_to_msg)?;
504
505 for (defn, _loc) in items {
507 let name = defn.get("name").and_then(|v| v.as_str()).unwrap_or("");
508 if !crate::permission::can_register(&self.policy.meta_policy, ctx) {
509 return Err(format!("ERR_PERMISSION: 无权注册或覆盖定义 {name}"));
510 }
511 }
512
513 struct Planned {
515 name: String,
516 schema: Schema,
517 links: Vec<Location>,
518 }
519 let mut planned: Vec<Planned> = Vec::new();
520 for name in &class.order {
521 let rs: Vec<Location> = class
522 .replicas
523 .get(name)
524 .map(|v| v.iter().map(|(_, l)| l.clone()).collect())
525 .unwrap_or_default();
526 if let Some((raw, loc, collection)) = class.primaries.get(name) {
527 let obj = raw.as_object().unwrap();
528 let schema = build_schema(obj, name.clone(), collection.clone(), loc)?;
529 let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
530 let mut links = vec![loc.clone()];
531 links.extend(rs);
532 planned.push(Planned {
533 name: name.clone(),
534 schema: schema.clone(),
535 links: links.clone(),
536 });
537 if !is_archive && !name.ends_with("Deleted") {
539 let aobj = archive_defn(obj, name, &schema.collection)
540 .as_object()
541 .cloned()
542 .unwrap();
543 let aname = format!("{}Deleted", name);
544 let acoll = format!("{}_deleted", schema.collection);
545 let aschema = build_schema(&aobj, aname.clone(), acoll, &links[0])?;
546 planned.push(Planned {
547 name: aname,
548 schema: aschema,
549 links,
550 });
551 }
552 } else {
553 let Some(existing) = self.catalog.entries.get(name) else {
555 return Err(format!(
556 "未找到主 schema: {}(从定义 `replica` 必须伴随同批主定义,或该 name 已注册)",
557 name
558 ));
559 };
560 let mut links = existing.links.clone();
561 links.extend(rs);
562 planned.push(Planned {
563 name: name.clone(),
564 schema: existing.schema.clone(),
565 links,
566 });
567 }
568 }
569
570 let batch_names: std::collections::HashSet<&str> =
572 planned.iter().map(|p| p.name.as_str()).collect();
573 let mut quads: Vec<(String, String, Location)> = Vec::new();
574 for (n, e) in &self.catalog.entries {
575 if batch_names.contains(n.as_str()) {
576 continue; }
578 for l in &e.links {
579 quads.push((n.clone(), e.schema.collection.clone(), l.clone()));
580 }
581 }
582 for p in &planned {
583 for l in &p.links {
584 quads.push((p.name.clone(), p.schema.collection.clone(), l.clone()));
585 }
586 }
587 if let Some(c) = detect_location_conflict(&quads) {
588 return Err(format!(
589 "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
590 c.source, c.database, c.schema, c.collection, c.prev
591 ));
592 }
593
594 let catalog = Arc::make_mut(&mut self.catalog); for p in planned {
597 if let Some(e) = catalog.entries.get_mut(&p.name) {
598 e.schema = p.schema;
599 e.links = p.links;
600 e.version += 1;
601 } else {
602 catalog.entries.insert(
603 p.name.clone(),
604 Entry {
605 schema: p.schema,
606 links: p.links,
607 version: 1,
608 },
609 );
610 catalog.order.push(p.name);
611 }
612 }
613
614 Ok(())
615 }
616
617 pub fn links_of(&self, name: &str) -> Result<&[Location], String> {
619 Ok(&self.get_entry(name)?.links)
620 }
621
622 pub fn primary_location(&self, name: &str) -> Result<&Location, String> {
624 self.get_entry(name)?
625 .links
626 .first()
627 .ok_or_else(|| format!("Schema 无落点链路: {}", name))
628 }
629
630 pub fn version_of(&self, name: &str) -> Result<u64, String> {
632 Ok(self.get_entry(name)?.version)
633 }
634
635 fn get_entry(&self, name: &str) -> Result<&Entry, String> {
636 self.catalog
637 .entries
638 .get(name)
639 .ok_or_else(|| format!("Schema 未注册: {}", name))
640 }
641
642 pub fn get(&self, name: &str) -> Result<&Schema, String> {
644 self.get_entry(name).map(|e| &e.schema)
645 }
646
647 pub fn has(&self, name: &str) -> bool {
648 self.catalog.entries.contains_key(name)
649 }
650
651 pub fn clear(&mut self) {
660 if let Err(e) = self.ensure_base_catalog() {
661 panic!("{e}");
662 }
663 let catalog = Arc::make_mut(&mut self.catalog);
664 catalog.entries.clear();
665 catalog.order.clear();
666 }
667
668 pub fn set_require_context(&mut self, require: bool) {
672 self.policy.require_context = require;
673 }
674
675 pub fn require_context(&self) -> bool {
677 self.policy.require_context
678 }
679
680 pub fn set_profile(&mut self, profile: Profile) {
685 self.policy.profile = profile;
686 }
687
688 pub fn profile(&self) -> Profile {
690 self.policy.profile
691 }
692
693 pub fn set_exempt_roles(&mut self, roles: Vec<String>) {
695 self.policy.role_rules.exempt_roles = roles;
696 }
697
698 pub fn set_deny_write_roles(&mut self, roles: Vec<String>) {
700 self.policy.role_rules.deny_write_roles = roles;
701 }
702
703 pub fn set_unconfigured_policy(&mut self, policy: crate::permission::UnconfiguredPolicy) {
705 self.policy.role_rules.unconfigured = policy;
706 }
707
708 pub fn role_rules(&self) -> &crate::permission::RoleRules {
710 &self.policy.role_rules
711 }
712
713 pub fn set_meta_policy(&mut self, closed: bool, roles: Vec<String>) {
716 self.policy.meta_policy = crate::permission::MetaPolicy { closed, roles };
717 }
718
719 pub fn meta_policy(&self) -> &crate::permission::MetaPolicy {
721 &self.policy.meta_policy
722 }
723
724 pub fn set_write_link_policy(&mut self, policy: WriteLinkPolicy) {
729 self.policy.write_link_policy = policy;
730 }
731
732 pub fn write_link_policy(&self) -> WriteLinkPolicy {
734 self.policy.write_link_policy
735 }
736
737 pub fn can_register(&self, ctx: Option<&crate::permission::Context>) -> bool {
743 crate::permission::can_register(&self.policy.meta_policy, ctx)
744 }
745
746 pub fn set_rbac(&mut self, policy: Option<&Value>) -> Result<(), String> {
749 match policy {
750 None => {
751 self.policy.rbac = None;
752 Ok(())
753 }
754 Some(v) => {
755 let p = crate::rbac::RbacPolicy::from_json(v)?;
756 self.policy.rbac = Some(p);
757 Ok(())
758 }
759 }
760 }
761
762 pub fn rbac(&self) -> Option<&crate::rbac::RbacPolicy> {
764 self.policy.rbac.as_ref()
765 }
766
767 pub fn get_by_location(
772 &self,
773 source: &str,
774 database: Option<&str>,
775 schema: Option<&str>,
776 collection: &str,
777 ) -> Result<&Schema, String> {
778 let db = database.filter(|s| !s.is_empty());
779 let sc = schema.filter(|s| !s.is_empty());
780 self.catalog
781 .entries
782 .values()
783 .find(|e| {
784 e.schema.collection == collection
785 && e.links.iter().any(|l| {
786 l.source == source
787 && l.database.as_deref().filter(|s| !s.is_empty()) == db
788 && l.schema.as_deref().filter(|s| !s.is_empty()) == sc
789 })
790 })
791 .map(|e| &e.schema)
792 .ok_or_else(|| {
793 format!(
794 "Schema 未定位(source = {}, database = {:?}, schema = {:?}, collection = {})",
795 source, db, sc, collection
796 )
797 })
798 }
799
800 pub fn get_for_command(
807 &self,
808 source: &str,
809 database: Option<&str>,
810 schema: Option<&str>,
811 collection: &str,
812 ) -> Result<&Schema, String> {
813 if let Ok(s) = self.get_by_location(source, database, schema, collection) {
814 return Ok(s);
815 }
816 let candidates: Vec<&Entry> = self
817 .catalog
818 .entries
819 .values()
820 .filter(|e| {
821 e.schema.collection == collection && e.links.iter().any(|l| l.source == source)
822 })
823 .collect();
824 match candidates.len() {
825 0 => self.get_by_location(source, database, schema, collection),
826 1 => Ok(&candidates[0].schema),
827 _ => candidates
828 .iter()
829 .find(|e| e.schema.database().is_none() && e.schema.schema().is_none())
830 .map(|e| &e.schema)
831 .ok_or_else(|| {
832 format!(
833 "命令定位 ({}, {:?}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
834 source, database, schema, collection
835 )
836 }),
837 }
838 }
839
840 pub fn list(&self) -> Vec<String> {
841 self.catalog.order.clone()
842 }
843
844 pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
846 Ok(self.get(name)?.source.clone())
847 }
848
849 pub fn resolve_datasource(
854 &self,
855 schema_name: &str,
856 config: &Value,
857 ) -> Result<crate::datasource::DataSource, String> {
858 let declared = self.get(schema_name)?.source.as_deref();
859 let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
860 cfg.resolve(declared)
861 }
862}
863
864fn build_schema(
867 obj: &Map<String, Value>,
868 name: String,
869 collection: String,
870 loc: &Location,
871) -> Result<Schema, String> {
872 let timestamps = parse_timestamps(obj)?;
873 let mut fields = normalize_fields(obj.get("fields"))?;
874 if timestamps {
875 add_timestamp_fields(&mut fields);
876 }
877 let relations = parse_relations(obj);
878 let computes = parse_computes(obj, &relations)?;
880 let triggers = super::triggers::parse_triggers(obj, &name, &fields)?;
882 let mut ids: Vec<&str> = Vec::new();
885 ids.extend(fields.keys().map(String::as_str));
886 ids.extend(relations.keys().map(String::as_str));
887 ids.extend(computes.iter().map(|(k, _)| k.as_str()));
888 crate::naming::detect_conflicts(ids)?;
889 Ok(Schema {
890 name,
891 collection,
892 id_prefix: obj
893 .get("idPrefix")
894 .and_then(|v| v.as_str())
895 .unwrap_or("")
896 .to_string(),
897 timestamps,
898 fields,
899 relations,
900 computes,
901 triggers,
902 read: str_list(obj.get("read")),
903 write: str_list(obj.get("write")),
904 indexes: obj
905 .get("indexes")
906 .and_then(|v| v.as_array())
907 .cloned()
908 .unwrap_or_default(),
909 source: if loc.source == crate::datasource::DEFAULT_SOURCE {
910 None
911 } else {
912 Some(loc.source.clone())
913 },
914 database: loc.database.clone().filter(|s| !s.is_empty()),
915 schema: loc.schema.clone().filter(|s| !s.is_empty()),
916 })
917}
918
919fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
921 match obj.get("timestamps") {
922 None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
923 Some(Value::Bool(false)) => Ok(false),
924 Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
925 Some(Value::String(s)) => Err(format!(
926 "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
927 s
928 )),
929 Some(other) => Err(format!(
930 "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
931 other
932 )),
933 }
934}
935
936fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
938 for key in ["createdAt", "updatedAt"] {
939 fields.entry(key.to_string()).or_insert_with(|| FieldDef {
940 field_type: "number".to_string(),
941 required: false,
942 default: None,
943 read: None,
944 write: None,
945 fields: None,
946 strategy: None,
947 });
948 }
949}
950
951fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
952 let mut relations = HashMap::new();
953 if let Some(Value::Object(rm)) = obj.get("relations") {
954 for (key, val) in rm {
955 let vo = val.as_object();
956 let get_str = |k: &str| -> Option<String> {
957 vo.and_then(|o| o.get(k))
958 .and_then(|v| v.as_str())
959 .filter(|s| !s.is_empty())
960 .map(String::from)
961 };
962 relations.insert(
963 key.clone(),
964 RelationDef {
965 model: get_str("model").unwrap_or_default(),
966 rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
967 local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
968 foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
969 read: str_list(vo.and_then(|o| o.get("read"))),
970 },
971 );
972 }
973 }
974 relations
975}
976
977fn parse_computes(
978 obj: &Map<String, Value>,
979 relations: &HashMap<String, RelationDef>,
980) -> Result<Vec<(String, ComputeDef)>, String> {
981 let mut computes = Vec::new();
982 if let Some(Value::Object(cm)) = obj.get("computes") {
983 for (key, val) in cm {
984 let vo = val.as_object();
985 let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
986 let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
987 let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
988 let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
989 computes.push((
990 key.clone(),
991 ComputeDef {
992 comp_type: get("type")
993 .and_then(|v| v.as_str().map(String::from))
994 .filter(|s| !s.is_empty())
995 .unwrap_or_else(|| "any".to_string()),
996 has_fn,
997 has_async_fn,
998 fn_ref: get("fnRef")
999 .and_then(|v| v.as_str().map(String::from))
1000 .filter(|s| !s.is_empty()),
1001 agg,
1002 depends: str_list(get("depends").as_ref()).unwrap_or_default(),
1003 read: str_list(get("read").as_ref()),
1004 },
1005 ));
1006 }
1007 }
1008 Ok(computes)
1009}
1010
1011fn parse_agg(
1019 key: &str,
1020 raw: Option<&Value>,
1021 relations: &HashMap<String, RelationDef>,
1022 has_callback: bool,
1023) -> Result<Option<Value>, String> {
1024 let Some(v) = raw.filter(|v| is_truthy(v)) else {
1025 return Ok(None);
1026 };
1027 if has_callback {
1028 return Err(format!(
1029 "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
1030 ));
1031 }
1032 let obj = v.as_object().ok_or_else(|| {
1033 format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
1034 })?;
1035 let mut it = obj.iter();
1036 let (op, arg) = match (it.next(), it.next()) {
1037 (Some(kv), None) => kv,
1038 _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
1039 };
1040 if !AGG_OPS.contains(&op.as_str()) {
1041 return Err(format!(
1042 "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
1043 ));
1044 }
1045 let path = arg
1046 .as_str()
1047 .filter(|s| !s.is_empty() && *s != "*")
1048 .ok_or_else(|| {
1049 format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
1050 })?;
1051 let (rel, field) = match path.split_once('.') {
1052 Some((r, f)) => (r, Some(f)),
1053 None => (path, None),
1054 };
1055 if !relations.contains_key(rel) {
1056 return Err(format!(
1057 "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
1058 ));
1059 }
1060 if field
1061 .map(|f| f.is_empty() || f.contains('.'))
1062 .unwrap_or(false)
1063 {
1064 return Err(format!(
1065 "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
1066 ));
1067 }
1068 match op.as_str() {
1069 "$count" if field.is_some() => Err(format!(
1070 "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
1071 )),
1072 "$count" => Ok(Some(v.clone())),
1073 _ if field.is_none() => Err(format!(
1074 "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
1075 )),
1076 _ => Ok(Some(v.clone())),
1077 }
1078}
1079
1080fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
1083 let mut arch_fields = obj
1084 .get("fields")
1085 .and_then(|v| v.as_object())
1086 .cloned()
1087 .unwrap_or_default();
1088 arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
1089 if let Some(idf) = arch_fields.get_mut("_id") {
1092 if let Some(o) = idf.as_object_mut() {
1093 o.remove("strategy");
1094 }
1095 }
1096 let arch = json!({
1097 "name": format!("{}Deleted", name),
1098 "collection": format!("{}_deleted", collection),
1099 "idPrefix": "",
1100 "_isArchive": true,
1101 "fields": Value::Object(arch_fields),
1102 "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
1103 });
1104 arch
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110
1111 #[test]
1112 fn write_link_policy_defaults_to_reject() {
1113 let r = Registry::new();
1114 assert_eq!(r.write_link_policy(), WriteLinkPolicy::Reject);
1115 let mut r2 = Registry::new();
1116 assert_eq!(r2.write_link_policy(), WriteLinkPolicy::Reject);
1117 r2.set_write_link_policy(WriteLinkPolicy::PrimaryOnly);
1118 assert_eq!(r2.write_link_policy(), WriteLinkPolicy::PrimaryOnly);
1119 }
1120
1121 use crate::permission::UnconfiguredPolicy;
1124
1125 fn overrides_all() -> PolicyOverrides {
1127 PolicyOverrides::from_value(&json!({
1128 "profile": "standard",
1129 "requireContext": false,
1130 "rbac": null,
1131 "roleRules": { "exemptRoles": ["x"], "denyWriteRoles": ["y"], "unconfigured": "open" },
1132 "metaPolicy": { "closed": false, "roles": [] },
1133 "writeLinkPolicy": "reject"
1134 }))
1135 .expect("合法覆盖 JSON")
1136 }
1137
1138 #[test]
1140 fn with_policy_override_matrix() {
1141 let mut base = Registry::new();
1142 base.set_require_context(true);
1143 base.set_profile(Profile::Text2Query);
1144 base.set_rbac(Some(&json!({}))).expect("合法 RBAC");
1145 base.set_exempt_roles(vec!["a".to_string()]);
1146 base.set_deny_write_roles(vec!["b".to_string()]);
1147 base.set_unconfigured_policy(UnconfiguredPolicy::Closed);
1148 base.set_meta_policy(true, vec!["m".to_string()]);
1149 base.set_write_link_policy(WriteLinkPolicy::PrimaryOnly);
1150
1151 let inherit = base.with_policy(&PolicyOverrides::default());
1153 assert!(inherit.is_view(), "派生视图 is_view 应为真");
1154 assert!(inherit.require_context());
1155 assert_eq!(inherit.profile(), Profile::Text2Query);
1156 assert!(inherit.rbac().is_some());
1157 assert_eq!(inherit.role_rules().exempt_roles, vec!["a".to_string()]);
1158 assert_eq!(inherit.role_rules().deny_write_roles, vec!["b".to_string()]);
1159 assert_eq!(
1160 inherit.role_rules().unconfigured,
1161 UnconfiguredPolicy::Closed
1162 );
1163 assert!(inherit.meta_policy().closed);
1164 assert_eq!(inherit.meta_policy().roles, vec!["m".to_string()]);
1165 assert_eq!(inherit.write_link_policy(), WriteLinkPolicy::PrimaryOnly);
1166
1167 let full = base.with_policy(&overrides_all());
1169 assert!(!full.require_context());
1170 assert_eq!(full.profile(), Profile::Standard);
1171 assert!(full.rbac().is_none(), "rbac:null 应清除");
1172 assert_eq!(full.role_rules().exempt_roles, vec!["x".to_string()]);
1173 assert_eq!(full.role_rules().deny_write_roles, vec!["y".to_string()]);
1174 assert_eq!(full.role_rules().unconfigured, UnconfiguredPolicy::Open);
1175 assert!(!full.meta_policy().closed);
1176 assert_eq!(full.write_link_policy(), WriteLinkPolicy::Reject);
1177
1178 assert!(base.require_context());
1180 assert_eq!(base.profile(), Profile::Text2Query);
1181 assert!(base.rbac().is_some());
1182 assert!(base.meta_policy().closed);
1183 }
1184
1185 #[test]
1187 fn view_register_is_readonly_err() {
1188 let base = Registry::new();
1189 let mut view = base.with_policy(&PolicyOverrides::default());
1190 let err = view
1191 .register(&json!({ "name": "A", "fields": {} }))
1192 .expect_err("视图注册应 Err");
1193 assert!(
1194 err.starts_with(ERR_POLICY_VIEW_READONLY),
1195 "前缀可匹配: {err}"
1196 );
1197 let err2 = view
1198 .register_with_ctx(&json!({ "name": "A", "fields": {} }), None)
1199 .expect_err("视图注册应 Err");
1200 assert!(
1201 err2.starts_with(ERR_POLICY_VIEW_READONLY),
1202 "前缀可匹配: {err2}"
1203 );
1204 let err3 = view
1205 .register_batch(
1206 &[(json!({ "name": "A", "fields": {} }), Location::default())],
1207 None,
1208 )
1209 .expect_err("视图批量注册应 Err");
1210 assert!(
1211 err3.starts_with(ERR_POLICY_VIEW_READONLY),
1212 "前缀可匹配: {err3}"
1213 );
1214 }
1215
1216 #[test]
1218 #[should_panic(expected = "ERR_POLICY_VIEW_READONLY")]
1219 fn view_clear_panics_readonly() {
1220 let base = Registry::new();
1221 let mut view = base.with_policy(&PolicyOverrides::default());
1222 view.clear();
1223 }
1224
1225 #[test]
1227 fn view_sees_catalog_snapshot() {
1228 let mut base = Registry::new();
1229 base.register(&json!({ "name": "A", "fields": { "x": { "type": "number" } } }))
1230 .expect("base 注册成功");
1231 let view = base.with_policy(&PolicyOverrides::default());
1232 assert!(view.has("A"), "派生视图应见 base 已注册目录");
1233 assert_eq!(view.get("A").expect("视图可取").name, "A");
1234 assert_eq!(view.list(), base.list());
1235
1236 let mut base2 = Registry::new();
1238 base2
1239 .register(&json!({ "name": "A", "fields": {} }))
1240 .unwrap();
1241 let snap = base2.with_policy(&PolicyOverrides::default());
1242 base2
1243 .register(&json!({ "name": "B", "fields": {} }))
1244 .unwrap();
1245 assert!(snap.has("A"), "在途视图仍持其快照");
1246 let fresh = base2.with_policy(&PolicyOverrides::default());
1247 assert!(fresh.has("B"), "新派生视图应见 base 后续注册");
1248 }
1249
1250 #[test]
1252 fn view_policy_set_isolated() {
1253 let base = Registry::new();
1254 let mut view = base.with_policy(&PolicyOverrides::default());
1255 view.set_profile(Profile::Text2Query);
1256 assert_eq!(view.profile(), Profile::Text2Query);
1257 assert_eq!(base.profile(), Profile::Standard, "视图 set 不影响 base");
1258 let sibling = base.with_policy(&PolicyOverrides::default());
1259 assert_eq!(sibling.profile(), Profile::Standard, "不影响兄弟视图");
1260 }
1261
1262 #[test]
1264 fn from_value_unknown_key_and_rbac_null() {
1265 let e = PolicyOverrides::from_value(&json!({ "nope": 1 })).expect_err("未知键应 Err");
1266 assert!(e.to_string().starts_with(ERR_POLICY_VIEW_READONLY), "{e}");
1267 assert!(
1268 PolicyOverrides::from_value(&json!({ "profile": "bogus" })).is_err(),
1269 "非法 profile 应 Err(禁静默回落)"
1270 );
1271 assert!(
1272 PolicyOverrides::from_value(&json!({ "writeLinkPolicy": "bogus" })).is_err(),
1273 "非法 writeLinkPolicy 应 Err"
1274 );
1275 assert!(
1276 PolicyOverrides::from_value(&json!({})).is_ok(),
1277 "空对象合法"
1278 );
1279
1280 let mut base = Registry::new();
1281 base.set_rbac(Some(&json!({}))).unwrap();
1282 let cleared =
1283 base.with_policy(&PolicyOverrides::from_value(&json!({ "rbac": null })).unwrap());
1284 assert!(cleared.rbac().is_none(), "rbac:null 应清除");
1285 let kept = base.with_policy(&PolicyOverrides::from_value(&json!({})).unwrap());
1286 assert!(kept.rbac().is_some(), "缺键应继承");
1287 assert!(base.rbac().is_some(), "base 不受影响");
1288 }
1289}