1use serde_json::{json, Map, Value};
8
9use crate::command::cmd::{cmd_delete_many, 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
108pub fn expand_schedule_triggers(
115 registry: &Registry,
116 ctx: Option<&Context>,
117) -> Result<Vec<Value>, String> {
118 let mut out = Vec::new();
119 let mut t2q_checked = false;
120 for owner in registry.list() {
121 let schema = registry.get(&owner)?;
122 let Some(list) = schema.triggers.get("schedule") else {
123 continue;
124 };
125 if list.is_empty() {
126 continue;
127 }
128 if !t2q_checked {
130 forbid_t2q(registry, "triggers")?;
131 t2q_checked = true;
132 }
133 for (i, td) in list.iter().enumerate() {
134 let step = build_trigger_step(registry, ctx, &owner, "schedule", i, td)?;
135 let name = step
136 .get("name")
137 .cloned()
138 .unwrap_or(Value::Null);
139 out.push(json!({
140 "schema": owner,
141 "name": name,
142 "cron": td.cron,
143 "step": step,
144 }));
145 }
146 }
147 Ok(out)
148}
149
150fn build_trigger_step(
151 registry: &Registry,
152 ctx: Option<&Context>,
153 owner: &str,
154 event: &str,
155 idx: usize,
156 td: &TriggerDef,
157) -> Result<Value, String> {
158 let name = if td.name.is_empty() {
159 format!("{owner}.{event}[{idx}]")
160 } else {
161 format!("{owner}.{event}.{}", td.name)
162 };
163 let base = |body: Value| -> Value {
164 let mut m = Map::new();
165 m.insert("name".into(), json!(name));
166 m.insert("onFields".into(), json!(td.on_fields));
167 m.insert("when".into(), td.when.clone().unwrap_or(Value::Null));
168 if let Value::Object(o) = body {
169 for (k, v) in o {
170 m.insert(k, v);
171 }
172 }
173 Value::Object(m)
174 };
175 match &td.body {
176 TriggerBody::Command {
177 into,
178 op,
179 condition,
180 data,
181 } => {
182 let target = registry.get(into)?;
183 if !can_write_schema(registry.role_rules(), target, ctx) {
184 return Err(ERR_NO_WRITE.to_string());
185 }
186 ensure_write(
187 registry,
188 target,
189 ctx,
190 match op.as_str() {
191 "insert" => WriteAction::Insert,
192 "remove" => WriteAction::Remove,
193 _ => WriteAction::Update,
194 },
195 )?;
196 let tfields: Vec<&String> = target.fields.keys().collect();
200 if let Value::Object(d) = data {
201 for k in d.keys() {
202 if k != "_id" && !k.starts_with('$') && !tfields.contains(&k) {
203 return Err(format!(
204 "触发器 \"{name}\" 的 data 字段 \"{k}\" 未在目标 schema \"{into}\" 中声明"
205 ));
206 }
207 }
208 }
209 let command = match op.as_str() {
210 "insert" => cmd_insert_one(target, data),
211 "update" => cmd_update_many(target, condition.as_ref().unwrap_or(&json!({})), data),
212 "remove" => cmd_delete_many(target, condition.as_ref().unwrap_or(&json!({}))),
214 other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
215 };
216 Ok(base(json!({ "command": command })))
217 }
218 TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
219 "callback": { "fnRef": fn_ref, "args": args }
220 }))),
221 }
222}