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
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            // 字段声明校验(值可能含占位符 → 只校验键,不校验类型);
197            // `_id` 豁免 —— 主键由 Host 显式供给(idPrefix/占位符),与普通 insert
198            // 文档显式携带 `_id` 同一契约(schema.fields 不声明 `_id`,见 registry.rs)
199            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            // update/remove 目标补行级探针:静态 owner 条件叠加 RBAC 行条件后并入
210            // filter(与写路径 `merge_row_condition` 同源;匹配不到即 0 行/拒绝),
211            // 杜绝触发器 update/remove 静默命中他人行。insert 的行级写覆盖由
212            // `cmd_insert_one`(filter_writable_data_overlay)承接,不在此叠加。
213            let base_condition = condition.as_ref().cloned().unwrap_or_else(|| json!({}));
214            let row_filtered = |action: &str| -> Value {
215                let owner_merged = merge_owner_condition(
216                    registry.role_rules(),
217                    target,
218                    ctx,
219                    Some(base_condition.clone()),
220                );
221                merge_row_condition(registry, target, ctx, action, owner_merged)
222                    .unwrap_or_else(|| json!({}))
223            };
224            let command = match op.as_str() {
225                "insert" => cmd_insert_one(target, data),
226                "update" => cmd_update_many(target, &row_filtered("update"), data),
227                // remove:condition 圈定删除目标,无 data(parse 已拒)
228                "remove" => cmd_delete_many(target, &row_filtered("remove")),
229                other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
230            };
231            Ok(base(json!({ "command": command })))
232        }
233        TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
234            "callback": { "fnRef": fn_ref, "args": args }
235        }))),
236    }
237}