rust_store_core/schema/
triggers.rs1use std::collections::HashMap;
8
9use serde_json::{Map, Value};
10
11use crate::types::str_list;
12
13pub const EVENTS: [&str; 2] = ["insert", "update"];
15
16pub const OPS: [&str; 2] = ["insert", "update"];
18
19pub type Triggers = HashMap<String, Vec<TriggerDef>>;
21
22#[derive(Debug, Clone)]
23pub struct TriggerDef {
24 pub name: String,
26 pub when: Option<Value>,
28 pub on_fields: Vec<String>,
30 pub body: TriggerBody,
31}
32
33#[derive(Debug, Clone)]
34pub enum TriggerBody {
35 Command {
37 into: String,
38 op: String, condition: Option<Value>, data: Value, },
42 Callback { fn_ref: String, args: Value },
44}
45
46pub fn parse_triggers(
59 obj: &Map<String, Value>,
60 owner: &str,
61 fields: &HashMap<String, crate::schema::FieldDef>,
62) -> Result<Triggers, String> {
63 let mut out: Triggers = HashMap::new();
64 let Some(Value::Object(tm)) = obj.get("triggers") else {
65 return Ok(out);
66 };
67 for (event, val) in tm {
68 if !EVENTS.contains(&event.as_str()) {
69 return Err(format!(
70 "schema \"{owner}\" 的 triggers 事件键 \"{event}\" 非法(仅 insert/update)"
71 ));
72 }
73 let arr = val
74 .as_array()
75 .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event} 必须是数组"))?;
76 let mut list = Vec::new();
77 for (i, t) in arr.iter().enumerate() {
78 let to = t
79 .as_object()
80 .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event}[{i}] 必须是对象"))?;
81 list.push(parse_one(owner, event, i, to, fields)?);
82 }
83 out.insert(event.clone(), list);
84 }
85 Ok(out)
86}
87
88fn parse_one(
89 owner: &str,
90 event: &str,
91 idx: usize,
92 to: &Map<String, Value>,
93 fields: &HashMap<String, crate::schema::FieldDef>,
94) -> Result<TriggerDef, String> {
95 let at = format!("{owner}.triggers.{event}[{idx}]");
96 let has_cmd = to.contains_key("into") || to.contains_key("op") || to.contains_key("data");
97 let has_cb = to.contains_key("fnRef");
98 if has_cmd == has_cb {
99 return Err(format!(
100 "{at} 必须二选一:命令式(into+op+data)或回调式(fnRef),不可同给或同缺"
101 ));
102 }
103 let on_fields = str_list(to.get("onFields")).unwrap_or_default();
105 for f in &on_fields {
106 if !f.starts_with('$') && !fields.contains_key(f) {
107 return Err(format!("{at} 的 onFields 字段 \"{f}\" 未在 schema 中声明"));
108 }
109 }
110 if let Some(w) = to.get("when") {
111 if !w.is_object() {
112 return Err(format!("{at} 的 when 必须是对象"));
113 }
114 }
115 if to.contains_key("cascade") {
116 return Err(format!("{at} 不支持 cascade(首批触发链只展开一层)"));
117 }
118 let name = to
119 .get("name")
120 .and_then(|v| v.as_str())
121 .filter(|s| !s.is_empty())
122 .unwrap_or("")
123 .to_string();
124
125 let body = if has_cb {
126 TriggerBody::Callback {
127 fn_ref: to
128 .get("fnRef")
129 .and_then(|v| v.as_str())
130 .filter(|s| !s.is_empty())
131 .ok_or_else(|| format!("{at} 的 fnRef 须为非空字符串"))?
132 .to_string(),
133 args: to.get("args").cloned().unwrap_or(Value::Null),
134 }
135 } else {
136 let into = to
137 .get("into")
138 .and_then(|v| v.as_str())
139 .filter(|s| !s.is_empty())
140 .ok_or_else(|| format!("{at} 缺少 into"))?
141 .to_string();
142 let op = to
143 .get("op")
144 .and_then(|v| v.as_str())
145 .ok_or_else(|| format!("{at} 缺少 op"))?
146 .to_string();
147 if !OPS.contains(&op.as_str()) {
148 return Err(format!("{at} 的 op \"{op}\" 不在白名单(insert/update)"));
149 }
150 let data = to
151 .get("data")
152 .filter(|v| v.is_object())
153 .cloned()
154 .ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?;
155 let condition = to.get("condition").cloned();
156 if op == "update" && !condition.as_ref().map(|v| v.is_object()).unwrap_or(false) {
157 return Err(format!("{at} 的 op=update 必须提供 condition(对象)"));
158 }
159 TriggerBody::Command {
160 into,
161 op,
162 condition,
163 data,
164 }
165 };
166 Ok(TriggerDef {
167 name,
168 when: to.get("when").cloned(),
169 on_fields,
170 body,
171 })
172}