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, merge_owner_condition, Context};
11use crate::rbac::{ensure_write, merge_row_condition, 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.get("name").cloned().unwrap_or(Value::Null);
136 out.push(json!({
137 "schema": owner,
138 "name": name,
139 "cron": td.cron,
140 "step": step,
141 }));
142 }
143 }
144 Ok(out)
145}
146
147fn build_trigger_step(
148 registry: &Registry,
149 ctx: Option<&Context>,
150 owner: &str,
151 event: &str,
152 idx: usize,
153 td: &TriggerDef,
154) -> Result<Value, String> {
155 let name = if td.name.is_empty() {
156 format!("{owner}.{event}[{idx}]")
157 } else {
158 format!("{owner}.{event}.{}", td.name)
159 };
160 let base = |body: Value| -> Value {
161 let mut m = Map::new();
162 m.insert("name".into(), json!(name));
163 m.insert("onFields".into(), json!(td.on_fields));
164 m.insert("when".into(), td.when.clone().unwrap_or(Value::Null));
165 if let Value::Object(o) = body {
166 for (k, v) in o {
167 m.insert(k, v);
168 }
169 }
170 Value::Object(m)
171 };
172 match &td.body {
173 TriggerBody::Command {
174 into,
175 op,
176 condition,
177 data,
178 } => {
179 let target = registry.get(into)?;
180 if !can_write_schema(registry.role_rules(), target, ctx) {
181 return Err(ERR_NO_WRITE.to_string());
182 }
183 ensure_write(
184 registry,
185 target,
186 ctx,
187 match op.as_str() {
188 "insert" => WriteAction::Insert,
189 "remove" => WriteAction::Remove,
190 _ => WriteAction::Update,
191 },
192 )?;
193 let tfields: Vec<&String> = target.fields.keys().collect();
197 if let Value::Object(d) = data {
198 for k in d.keys() {
199 if k != "_id" && !k.starts_with('$') && !tfields.contains(&k) {
200 return Err(format!(
201 "触发器 \"{name}\" 的 data 字段 \"{k}\" 未在目标 schema \"{into}\" 中声明"
202 ));
203 }
204 }
205 }
206 let base_condition = condition.as_ref().cloned().unwrap_or_else(|| json!({}));
211 let row_filtered = |action: &str| -> Value {
212 let owner_merged = merge_owner_condition(
213 registry.role_rules(),
214 target,
215 ctx,
216 Some(base_condition.clone()),
217 );
218 merge_row_condition(registry, target, ctx, action, owner_merged)
219 .unwrap_or_else(|| json!({}))
220 };
221 let command = match op.as_str() {
222 "insert" => cmd_insert_one(target, data),
223 "update" => cmd_update_many(target, &row_filtered("update"), data),
224 "remove" => cmd_delete_many(target, &row_filtered("remove")),
226 other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
227 };
228 Ok(base(json!({ "command": command })))
229 }
230 TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
231 "callback": { "fnRef": fn_ref, "args": args }
232 }))),
233 }
234}