rust_store_core/command/
triggers.rs1use serde_json::{json, Map, Value};
8
9use crate::command::cmd::{cmd_insert_one, cmd_update_many};
10use crate::permission::{can_write_schema, Context};
11use crate::rbac::{ensure_write, WriteAction};
12use crate::schema::{Registry, TriggerBody, TriggerDef};
13
14use super::{forbid_t2q, ERR_NO_WRITE};
15
16pub fn has_triggers(registry: &Registry, schema_name: &str, event: &str) -> bool {
18 registry
19 .get(schema_name)
20 .ok()
21 .and_then(|s| s.triggers.get(event))
22 .map(|v| !v.is_empty())
23 .unwrap_or(false)
24}
25
26pub fn before_probe_fields(triggers: &[TriggerDef]) -> Vec<String> {
28 let mut out: Vec<String> = Vec::new();
29 let mut push = |f: &str| {
30 if !f.is_empty() && !out.contains(&f.to_string()) {
31 out.push(f.to_string());
32 }
33 };
34 for td in triggers {
35 for f in &td.on_fields {
36 push(f);
37 }
38 collect_before_refs(td.when.as_ref(), &mut push);
39 if let TriggerBody::Callback { args, .. } = &td.body {
40 collect_before_refs(Some(args), &mut push);
41 }
42 if let TriggerBody::Command {
43 data, condition, ..
44 } = &td.body
45 {
46 collect_before_refs(Some(data), &mut push);
47 collect_before_refs(condition.as_ref(), &mut push);
48 }
49 }
50 out
51}
52
53fn collect_before_refs(v: Option<&Value>, push: &mut impl FnMut(&str)) {
55 match v {
56 Some(Value::String(s)) => {
57 let mut rest = s.as_str();
58 while let Some(p) = rest.find("{{before.") {
59 let after = &rest[p + "{{before.".len()..];
60 if let Some(end) = after.find("}}") {
61 let field = after[..end].trim();
62 let key = field.split(['.', '[']).next().unwrap_or(field);
63 push(key);
64 rest = &after[end + 2..];
65 } else {
66 break;
67 }
68 }
69 }
70 Some(Value::Array(a)) => a.iter().for_each(|x| collect_before_refs(Some(x), push)),
71 Some(Value::Object(o)) => o.values().for_each(|x| collect_before_refs(Some(x), push)),
72 _ => {}
73 }
74}
75
76pub fn expand_triggers(
81 registry: &Registry,
82 ctx: Option<&Context>,
83 owner_schema: &str,
84 event: &str,
85) -> Result<Vec<Value>, String> {
86 let schema = registry.get(owner_schema)?;
87 let Some(list) = schema.triggers.get(event) else {
88 return Ok(Vec::new());
89 };
90 if !list.is_empty() {
92 forbid_t2q(registry, "triggers")?;
93 }
94 let mut out = Vec::new();
95 for (i, td) in list.iter().enumerate() {
96 out.push(build_trigger_step(
97 registry,
98 ctx,
99 owner_schema,
100 event,
101 i,
102 td,
103 )?);
104 }
105 Ok(out)
106}
107
108fn build_trigger_step(
109 registry: &Registry,
110 ctx: Option<&Context>,
111 owner: &str,
112 event: &str,
113 idx: usize,
114 td: &TriggerDef,
115) -> Result<Value, String> {
116 let name = if td.name.is_empty() {
117 format!("{owner}.{event}[{idx}]")
118 } else {
119 format!("{owner}.{event}.{}", td.name)
120 };
121 let base = |body: Value| -> Value {
122 let mut m = Map::new();
123 m.insert("name".into(), json!(name));
124 m.insert("onFields".into(), json!(td.on_fields));
125 m.insert("when".into(), td.when.clone().unwrap_or(Value::Null));
126 if let Value::Object(o) = body {
127 for (k, v) in o {
128 m.insert(k, v);
129 }
130 }
131 Value::Object(m)
132 };
133 match &td.body {
134 TriggerBody::Command {
135 into,
136 op,
137 condition,
138 data,
139 } => {
140 let target = registry.get(into)?;
141 if !can_write_schema(registry.role_rules(), target, ctx) {
142 return Err(ERR_NO_WRITE.to_string());
143 }
144 ensure_write(
145 registry,
146 target,
147 ctx,
148 if op == "insert" {
149 WriteAction::Insert
150 } else {
151 WriteAction::Update
152 },
153 )?;
154 let tfields: Vec<&String> = target.fields.keys().collect();
158 if let Value::Object(d) = data {
159 for k in d.keys() {
160 if k != "_id" && !k.starts_with('$') && !tfields.contains(&k) {
161 return Err(format!(
162 "触发器 \"{name}\" 的 data 字段 \"{k}\" 未在目标 schema \"{into}\" 中声明"
163 ));
164 }
165 }
166 }
167 let command = match op.as_str() {
168 "insert" => cmd_insert_one(target, data),
169 "update" => cmd_update_many(target, condition.as_ref().unwrap_or(&json!({})), data),
170 other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
171 };
172 Ok(base(json!({ "command": command })))
173 }
174 TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
175 "callback": { "fnRef": fn_ref, "args": args }
176 }))),
177 }
178}