Skip to main content

rust_store_core/command/
mutation.rs

1//! mutation 规划器(对应 JS `_mutationOne` + `_applyRelations`)
2//!
3//! 把一次 mutation 展开为**有序步骤序列**:每个步骤是一条命令(insertOne 或
4//! findOneAndUpdate),父子步骤间的依赖(子文档外键 = 父文档 `_id`)用占位符表达:
5//!
6//! - `{{step.<N>._id}}`(见 [`step_id_placeholder`]):第 N 步执行结果文档的 `_id`,
7//!   Host 在该步执行完成后回填到后续命令中
8//! - 需要新生成 `_id` 的位置由 Host 供给的 `new_ids` 按序填充(与 JS `_generateId`
9//!   的调用顺序一致;core 无随机源)
10//!
11//! 根步骤永远是 `steps[0]`;Host 依次执行后,用根步骤结果文档调
12//! `apply_defaults_and_computes` 得到最终返回值。
13
14use serde_json::{json, Map, Value};
15
16use crate::permission::{can_write_schema, evaluate, filter_writable_data, Context, Doc};
17use crate::schema::Registry;
18
19use super::cmd::{cmd_find_one_and_update, cmd_insert_one};
20use super::mutate::{
21    build_upsert_conditions, build_upsert_update, needs_new_id, object_of, upsert_one_update,
22    IdCursor,
23};
24use super::write::build_insert_doc;
25use super::{ensure_context, step_id_placeholder, ERR_NO_WRITE};
26
27/// 规划一条 mutation(对应 JS `mutation` 的单条分支 `_mutationOne`)。
28///
29/// 数组形态与空数组短路(`[]` / `null`)由 Host 处理:逐条调用本函数即可;
30/// `new_ids` 由多条 mutation 共享一个游标语义(每次调用传剩余数组)。
31pub fn plan_mutation(
32    schema_name: &str,
33    registry: &Registry,
34    ctx: Option<&Context>,
35    data: &Value,
36    now: i64,
37    new_ids: &[String],
38) -> Result<Value, String> {
39    ensure_context(registry, ctx)?;
40    let mut out = PlanOut::default();
41    let mut ids = IdCursor::new(new_ids);
42    plan_mutation_node(schema_name, registry, ctx, data, now, &mut ids, &mut out)?;
43    Ok(json!({ "steps": out.steps, "degraded": out.degraded }))
44}
45
46/// 规划期的可变累积器:有序步骤序列 + 降级声明(§11.4 静默点收口)。
47#[derive(Default)]
48struct PlanOut {
49    steps: Vec<Value>,
50    degraded: Vec<Value>,
51}
52
53/// 展开一个 mutation 节点:根写入步骤 + 各 relation 子步骤(`many` 递归)
54fn plan_mutation_node(
55    schema_name: &str,
56    registry: &Registry,
57    ctx: Option<&Context>,
58    data: &Value,
59    now: i64,
60    ids: &mut IdCursor,
61    out: &mut PlanOut,
62) -> Result<(), String> {
63    let schema = registry.get(schema_name)?;
64    if !can_write_schema(schema, ctx) {
65        return Err(ERR_NO_WRITE.to_string());
66    }
67    let Some(obj) = data.as_object() else {
68        return Err("mutation 数据必须是对象".to_string());
69    };
70
71    // ── 1. 按 schema.relations 拆分 fieldData + relationData(保持遍历顺序) ──
72    let mut field_data = Map::new();
73    let mut relation_data: Vec<(&String, &Value)> = Vec::new();
74    for (key, val) in obj {
75        if let Some(rel_def) = schema.relations.get(key) {
76            // JS:relDef.read && !evaluate(ctx, relDef.read) → 跳过该关系
77            let allowed = match ctx {
78                None => true,
79                Some(c) => match &rel_def.read {
80                    Some(rl) => evaluate(Some(c), Some(rl), Doc::Missing),
81                    None => true,
82                },
83            };
84            if allowed {
85                relation_data.push((key, val));
86            } else {
87                // §11.4 静默点收口(D2:绝不静默):关系 read 不通过时写入数据被跳过,
88                // 但必须显式声明 —— 记入 `degraded:relationSkipped`,Host 经反馈通道告警。
89                out.degraded.push(json!({
90                    "code": "relationSkipped",
91                    "layer": "mutation",
92                    "message": format!(
93                        "模型 {schema_name} 的关系 \"{key}\" 不可读(read 权限不通过),其写入数据已跳过"
94                    ),
95                    "hint": "授予该关系 read 权限;如需该情形显式报错,请在应用层先校验权限",
96                }));
97            }
98        } else {
99            field_data.insert(key.clone(), val.clone());
100        }
101    }
102
103    let filtered_field = match ctx {
104        Some(_) => filter_writable_data(schema, ctx, &Value::Object(field_data)),
105        None => Value::Object(field_data),
106    };
107
108    // ── 2/3. 写入 parent(upsert 或 insert 路径在规划期即可确定) ──
109    let step_idx = out.steps.len();
110    let parent_ph = step_id_placeholder(step_idx);
111    let or_conditions = build_upsert_conditions(schema, &filtered_field);
112    let command = if or_conditions.is_empty() {
113        // Insert 路径(复用 JS `insert` 逻辑;仅需要新 `_id` 时才消耗游标)
114        let new_id = if needs_new_id(schema, &filtered_field) {
115            ids.next()?
116        } else {
117            ""
118        };
119        let doc = build_insert_doc(schema, ctx, &filtered_field, now, new_id)?;
120        cmd_insert_one(schema, &Value::Object(doc))
121    } else {
122        // Upsert 路径(JS `_buildUpsertUpdate` 仅在无 _id 时才 `_generateId`)
123        let new_id = if needs_new_id(schema, &filtered_field) {
124            ids.next()?
125        } else {
126            ""
127        };
128        let update_doc = build_upsert_update(schema, &filtered_field, new_id, now);
129        let fu_options = json!({ "upsert": true, "returnDocument": "after" });
130        cmd_find_one_and_update(
131            schema,
132            &json!({ "$or": or_conditions }),
133            &update_doc,
134            &fu_options,
135        )
136    };
137    out.steps
138        .push(json!({ "model": schema_name, "command": command }));
139
140    // ── 4. 处理 relation 子文档 ──
141    for (rel_name, rel_val) in relation_data {
142        if rel_val.is_null() {
143            continue;
144        }
145        // checked 索引:数据键未在 relations 定义时显式报错,绝不 panic
146        let rel_def = schema
147            .relations
148            .get(rel_name)
149            .ok_or_else(|| format!("模型 {} 的数据含未定义关系字段 {}", schema_name, rel_name))?;
150        let rel_schema = registry.get(&rel_def.model)?;
151
152        if rel_def.rel_type == "one" {
153            // type:'one' 子文档强制按 foreignKey upsert(写入形态对齐 JS `_upsertOne`;
154            // 权限门禁与字段过滤与 many 路径对齐:can_write_schema + filter_writable_data,
155            // 拥有父模型写权限 ≠ 拥有子模型写权限)
156            if !can_write_schema(rel_schema, ctx) {
157                return Err(ERR_NO_WRITE.to_string());
158            }
159            let mut child = object_of(rel_val);
160            child.insert(rel_def.foreign_field.clone(), json!(parent_ph));
161            // 注入外键后再过滤(与 many 路径 plan_mutation_node 的过滤时机一致)
162            let child_val = match ctx {
163                Some(_) => filter_writable_data(rel_schema, ctx, &Value::Object(child)),
164                None => Value::Object(child),
165            };
166            // JS `_upsertOne`:仅无 _id 时才 `_generateId`
167            let new_id = if needs_new_id(rel_schema, &child_val) {
168                ids.next()?
169            } else {
170                ""
171            };
172            let update_doc = upsert_one_update(rel_schema, &child_val, new_id, now);
173            let filter = json!({ rel_def.foreign_field.clone(): parent_ph });
174            let fu_options = json!({ "upsert": true, "returnDocument": "after" });
175            let cmd = cmd_find_one_and_update(rel_schema, &filter, &update_doc, &fu_options);
176            out.steps
177                .push(json!({ "model": rel_def.model, "command": cmd }));
178        } else if rel_def.rel_type == "many" {
179            let arr: Vec<Value> = match rel_val {
180                Value::Array(a) => a.clone(),
181                _ => vec![rel_val.clone()],
182            };
183            for child in arr {
184                if child.is_null() {
185                    continue;
186                }
187                let mut c = object_of(&child);
188                c.insert(rel_def.foreign_field.clone(), json!(parent_ph));
189                plan_mutation_node(
190                    &rel_def.model,
191                    registry,
192                    ctx,
193                    &Value::Object(c),
194                    now,
195                    ids,
196                    out,
197                )?;
198            }
199        } else {
200            // §11.4 静默点收口(D2:绝不静默):未知 rel_type 无法规划写入步骤,
201            // 显式报错(原行为为静默跳过该关系)。
202            return Err(format!(
203                "关系 \"{rel_name}\" 的 rel_type \"{}\" 不被支持(仅支持 one / many;D2:绝不静默跳过)",
204                rel_def.rel_type
205            ));
206        }
207    }
208
209    Ok(())
210}