use serde_json::{json, Map, Value};
use crate::command::cmd::{cmd_delete_many, cmd_insert_one, cmd_update_many};
use crate::permission::{can_write_schema, merge_owner_condition, Context};
use crate::rbac::{ensure_write, merge_row_condition, WriteAction};
use crate::schema::{Registry, TriggerBody, TriggerDef};
use super::{forbid_t2q, ERR_NO_WRITE};
pub fn has_triggers(registry: &Registry, schema_name: &str, event: &str) -> bool {
registry
.get(schema_name)
.ok()
.and_then(|s| s.triggers.get(event))
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn before_probe_fields(triggers: &[TriggerDef]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut push = |f: &str| {
if !f.is_empty() && !out.contains(&f.to_string()) {
out.push(f.to_string());
}
};
for td in triggers {
for f in &td.on_fields {
push(f);
}
collect_before_refs(td.when.as_ref(), &mut push);
if let TriggerBody::Callback { args, .. } = &td.body {
collect_before_refs(Some(args), &mut push);
}
if let TriggerBody::Command {
data, condition, ..
} = &td.body
{
collect_before_refs(Some(data), &mut push);
collect_before_refs(condition.as_ref(), &mut push);
}
}
out
}
fn collect_before_refs(v: Option<&Value>, push: &mut impl FnMut(&str)) {
match v {
Some(Value::String(s)) => {
let mut rest = s.as_str();
while let Some(p) = rest.find("{{before.") {
let after = &rest[p + "{{before.".len()..];
if let Some(end) = after.find("}}") {
let field = after[..end].trim();
let key = field.split(['.', '[']).next().unwrap_or(field);
push(key);
rest = &after[end + 2..];
} else {
break;
}
}
}
Some(Value::Array(a)) => a.iter().for_each(|x| collect_before_refs(Some(x), push)),
Some(Value::Object(o)) => o.values().for_each(|x| collect_before_refs(Some(x), push)),
_ => {}
}
}
pub fn expand_triggers(
registry: &Registry,
ctx: Option<&Context>,
owner_schema: &str,
event: &str,
) -> Result<Vec<Value>, String> {
let schema = registry.get(owner_schema)?;
let Some(list) = schema.triggers.get(event) else {
return Ok(Vec::new());
};
if !list.is_empty() {
forbid_t2q(registry, "triggers")?;
}
let mut out = Vec::new();
for (i, td) in list.iter().enumerate() {
out.push(build_trigger_step(
registry,
ctx,
owner_schema,
event,
i,
td,
)?);
}
Ok(out)
}
pub fn expand_schedule_triggers(
registry: &Registry,
ctx: Option<&Context>,
) -> Result<Vec<Value>, String> {
let mut out = Vec::new();
let mut t2q_checked = false;
for owner in registry.list() {
let schema = registry.get(&owner)?;
let Some(list) = schema.triggers.get("schedule") else {
continue;
};
if list.is_empty() {
continue;
}
if !t2q_checked {
forbid_t2q(registry, "triggers")?;
t2q_checked = true;
}
for (i, td) in list.iter().enumerate() {
let step = build_trigger_step(registry, ctx, &owner, "schedule", i, td)?;
let name = step
.get("name")
.cloned()
.unwrap_or(Value::Null);
out.push(json!({
"schema": owner,
"name": name,
"cron": td.cron,
"step": step,
}));
}
}
Ok(out)
}
fn build_trigger_step(
registry: &Registry,
ctx: Option<&Context>,
owner: &str,
event: &str,
idx: usize,
td: &TriggerDef,
) -> Result<Value, String> {
let name = if td.name.is_empty() {
format!("{owner}.{event}[{idx}]")
} else {
format!("{owner}.{event}.{}", td.name)
};
let base = |body: Value| -> Value {
let mut m = Map::new();
m.insert("name".into(), json!(name));
m.insert("onFields".into(), json!(td.on_fields));
m.insert("when".into(), td.when.clone().unwrap_or(Value::Null));
if let Value::Object(o) = body {
for (k, v) in o {
m.insert(k, v);
}
}
Value::Object(m)
};
match &td.body {
TriggerBody::Command {
into,
op,
condition,
data,
} => {
let target = registry.get(into)?;
if !can_write_schema(registry.role_rules(), target, ctx) {
return Err(ERR_NO_WRITE.to_string());
}
ensure_write(
registry,
target,
ctx,
match op.as_str() {
"insert" => WriteAction::Insert,
"remove" => WriteAction::Remove,
_ => WriteAction::Update,
},
)?;
let tfields: Vec<&String> = target.fields.keys().collect();
if let Value::Object(d) = data {
for k in d.keys() {
if k != "_id" && !k.starts_with('$') && !tfields.contains(&k) {
return Err(format!(
"触发器 \"{name}\" 的 data 字段 \"{k}\" 未在目标 schema \"{into}\" 中声明"
));
}
}
}
let base_condition = condition.as_ref().cloned().unwrap_or_else(|| json!({}));
let row_filtered = |action: &str| -> Value {
let owner_merged = merge_owner_condition(
registry.role_rules(),
target,
ctx,
Some(base_condition.clone()),
);
merge_row_condition(registry, target, ctx, action, owner_merged)
.unwrap_or_else(|| json!({}))
};
let command = match op.as_str() {
"insert" => cmd_insert_one(target, data),
"update" => cmd_update_many(target, &row_filtered("update"), data),
"remove" => cmd_delete_many(target, &row_filtered("remove")),
other => return Err(format!("触发器 \"{name}\" 的 op \"{other}\" 不支持")),
};
Ok(base(json!({ "command": command })))
}
TriggerBody::Callback { fn_ref, args } => Ok(base(json!({
"callback": { "fnRef": fn_ref, "args": args }
}))),
}
}