Skip to main content

rust_store_core/command/
triggers.rs

1//! 触发链规划:由 `Schema.triggers` 展开为命令 / 回调步骤序列。
2//!
3//! **规划期不持有 root 值**(root 是 Host 主写结果)——命令 filter/data / args / when
4//! 内的占位符 `{{root.*}}` / `{{before.*}}` / `{{now}}` 原样保留,由 Host 执行前替换
5//! (与 mutation `{{step.N._id}}` 同范式,见 `command/mod.rs`)。
6
7use 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
16/// 某 schema + 事件是否有触发器
17pub 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
26/// 探针需额外投影的字段:全部 `onFields` ∪ `when`/`data`/`args` 中出现的 `{{before.<f>}}`
27pub 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
53/// 递归收集字符串中的 `{{before.<f>}}` 引用
54fn 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
76/// 展开某 schema + 事件的触发链(只展开一层)。
77///
78/// 产出 step 列表;每个 step:
79///   `{ "name", "onFields", "when", "command"|"callback" }`
80pub 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    // text2query 档:写法副作用不得由 AI 问数触发(A9)
91    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
108/// 枚举全 registry 的 `schedule` 触发器(宿主定时任务插件消费)。
109///
110/// 产出 `[{ "schema", "name", "cron", "step" }]`;`step` 与写链触发步骤同构
111/// (`{ name, onFields, when, command|callback }`),占位符仅 `{{now}}`
112/// (root/before 已在注册期拒,见 `schema::triggers::parse_triggers`)。
113/// 按 `registry.list()` 顺序枚举、每 schema 按声明顺序,输出稳定可对拍。
114pub 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        // text2query 档:写法副作用不得由 AI 问数触发(A9),幂等防重
129        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            // 字段声明校验(值可能含占位符 → 只校验键,不校验类型);
194            // `_id` 豁免 —— 主键由 Host 显式供给(idPrefix/占位符),与普通 insert
195            // 文档显式携带 `_id` 同一契约(schema.fields 不声明 `_id`,见 registry.rs)
196            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            // update/remove 目标补行级探针:静态 owner 条件叠加 RBAC 行条件后并入
207            // filter(与写路径 `merge_row_condition` 同源;匹配不到即 0 行/拒绝),
208            // 杜绝触发器 update/remove 静默命中他人行。insert 的行级写覆盖由
209            // `cmd_insert_one`(filter_writable_data_overlay)承接,不在此叠加。
210            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:condition 圈定删除目标,无 data(parse 已拒)
225                "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}