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_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
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
108fn build_trigger_step(
109    registry: &Registry,
110    ctx: Option<&Context>,
111    owner: &str,
112    event: &str,
113    idx: usize,
114    td: &TriggerDef,
115) -> Result<Value, String> {
116    let name = if td.name.is_empty() {
117        format!("{owner}.{event}[{idx}]")
118    } else {
119        format!("{owner}.{event}.{}", td.name)
120    };
121    let base = |body: Value| -> Value {
122        let mut m = Map::new();
123        m.insert("name".into(), json!(name));
124        m.insert("onFields".into(), json!(td.on_fields));
125        m.insert("when".into(), td.when.clone().unwrap_or(Value::Null));
126        if let Value::Object(o) = body {
127            for (k, v) in o {
128                m.insert(k, v);
129            }
130        }
131        Value::Object(m)
132    };
133    match &td.body {
134        TriggerBody::Command {
135            into,
136            op,
137            condition,
138            data,
139        } => {
140            let target = registry.get(into)?;
141            if !can_write_schema(registry.role_rules(), target, ctx) {
142                return Err(ERR_NO_WRITE.to_string());
143            }
144            ensure_write(
145                registry,
146                target,
147                ctx,
148                if op == "insert" {
149                    WriteAction::Insert
150                } else {
151                    WriteAction::Update
152                },
153            )?;
154            // 字段声明校验(值可能含占位符 → 只校验键,不校验类型);
155            // `_id` 豁免 —— 主键由 Host 显式供给(idPrefix/占位符),与普通 insert
156            // 文档显式携带 `_id` 同一契约(schema.fields 不声明 `_id`,见 registry.rs)
157            let tfields: Vec<&String> = target.fields.keys().collect();
158            if let Value::Object(d) = data {
159                for k in d.keys() {
160                    if k != "_id" && !k.starts_with('$') && !tfields.contains(&k) {
161                        return Err(format!(
162                            "触发器 \"{name}\" 的 data 字段 \"{k}\" 未在目标 schema \"{into}\" 中声明"
163                        ));
164                    }
165                }
166            }
167            let command = match op.as_str() {
168                "insert" => cmd_insert_one(target, data),
169                "update" => cmd_update_many(target, condition.as_ref().unwrap_or(&json!({})), data),
170                other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
171            };
172            Ok(base(json!({ "command": command })))
173        }
174        TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
175            "callback": { "fnRef": fn_ref, "args": args }
176        }))),
177    }
178}