Skip to main content

rust_store_core/command/
write.rs

1//! 写路径与简单查询计划
2
3use serde_json::{json, Map, Value};
4
5use crate::computes::{apply_defaults_and_computes, FnRegistry};
6use crate::permission::{
7    can_write_schema, evaluate, merge_owner_condition, Context, Doc,
8};
9use crate::rbac::{
10    ensure_write, ensure_write_on_doc, filter_writable_data_overlay, merge_row_condition,
11    probe_condition_fields, WriteAction,
12};
13use crate::schema::{Registry, Schema};
14use crate::types::{
15    has_relation_predicate, is_truthy, validate_condition, validate_condition_shape,
16};
17
18use super::cmd::{cmd_count_documents, cmd_find_one, cmd_insert_one};
19use super::{ensure_context, ERR_NO_WRITE};
20
21/// 生成插入命令(对应 JS `insert`)
22///
23/// `now` 与 `new_id` 由 Host 提供(core 无时钟与随机源,且这样可让产出确定、便于对拍)。
24pub fn plan_insert(
25    schema_name: &str,
26    registry: &Registry,
27    ctx: Option<&Context>,
28    data: &Value,
29    now: i64,
30    new_id: &str,
31    fn_registry: Option<&dyn FnRegistry>,
32) -> Result<Value, String> {
33    ensure_context(registry, ctx)?;
34    let schema = registry.get(schema_name)?;
35
36    if !can_write_schema(registry.role_rules(), schema, ctx) {
37        return Err(ERR_NO_WRITE.to_string());
38    }
39    // RBAC 表级写判定(deny-wins,叠加于静态白名单之后)
40    ensure_write(registry, schema, ctx, WriteAction::Insert)?;
41
42    let doc = build_insert_doc(registry, schema, ctx, data, now, new_id)?;
43    let doc = Value::Object(doc);
44    Ok(json!({
45        "command": cmd_insert_one(schema, &doc),
46        "returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
47    }))
48}
49
50/// insert 文档规范化(对应 JS `insert` 主体):
51/// 权限过滤 → 保留显式 null → 自动 _id / createdBy / 时间戳
52pub(super) fn build_insert_doc(
53    registry: &Registry,
54    schema: &Schema,
55    ctx: Option<&Context>,
56    data: &Value,
57    now: i64,
58    new_id: &str,
59) -> Result<Map<String, Value>, String> {
60    let filtered = match ctx {
61        // RBAC 感知版:静态 writable ∩ RBAC writeFields(无策略时直通静态过滤)
62        Some(_) => filter_writable_data_overlay(registry, schema, ctx, data),
63        None => data.clone(),
64    };
65
66    // 保留显式 null:历史实现把 null 一律剔除(当成缺失),导致「空串/NULL/缺失」三态
67    // 无法区分(F-07),null 字段读回也被吞(H-01)。显式 `null` 应原样落库,SQL 落 NULL。
68    let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
69
70    // 自动生成 ID(仅在 schema 配了 idPrefix 且未提供有效 _id 时);
71    // 无 idPrefix 且无 _id → 显式报错(缺陷 D-03):静默产出「无 _id 文档」会让
72    // Host 返回 _id=undefined,且 Mongo 自动 ObjectId 会触发跨绑定序列化崩溃
73    if !doc.get("_id").map(is_truthy).unwrap_or(false) {
74        if schema.id_is_autoincrement() {
75            // 阶段2:数据库自增主键 —— doc 不注入 `_id`(SQL INSERT 列白名单来自 doc 键,
76            // 无 `_id` 列即由数据库赋值;PG/SQLite 走 RETURNING 回读、MySQL 走宿主
77            // lastrowid)。Mongo 无自增语义,由宿主 Mongo 执行器对此形态显式报错
78            // (AUTOINCREMENT_NOT_SUPPORTED),禁 ObjectId 静默顶替。
79        } else if !schema.id_prefix.is_empty() {
80            doc.insert("_id".to_string(), json!(new_id));
81        } else {
82            return Err(format!(
83                "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
84                schema.name
85            ));
86        }
87    }
88
89    // 自动设置 createdBy(creator 权限场景)
90    if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
91        if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
92            doc.insert("createdBy".to_string(), json!(uid));
93        }
94    }
95
96    // 自动时间戳
97    if schema.timestamps {
98        if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
99            doc.insert("createdAt".to_string(), json!(now));
100        }
101        doc.insert("updatedAt".to_string(), json!(now));
102    }
103    Ok(doc)
104}
105
106/// 判断存在性(对应 JS `exists`)
107pub fn plan_exists(
108    schema_name: &str,
109    registry: &Registry,
110    condition: &Value,
111) -> Result<Value, String> {
112    // 条件拒绝名单(缺陷 D-02)
113    validate_condition(condition)?;
114    let schema = registry.get(schema_name)?;
115    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
116    validate_condition_shape(schema, condition, registry.profile())?;
117    // §9.6 关系聚合谓词无法用标量 `findOne` 表达 → 显式 Err(否则 Mongo 静默给错结果)
118    if has_relation_predicate(schema, condition) {
119        return Err(
120            "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
121             请改用 query 并自行判断是否有结果"
122                .to_string(),
123        );
124    }
125    Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
126}
127
128/// 统计数量(对应 JS `count`):filter 为 nullish 时用 `{}`
129///
130/// R1:owner(read=creator) 场景下,count 必须注入 `createdBy = ctx.userId`,
131/// 否则非 admin 用户 count 会越权统计全表(E-08)。
132pub fn plan_count(
133    schema_name: &str,
134    registry: &Registry,
135    filter: Option<&Value>,
136    ctx: Option<&Context>,
137) -> Result<Value, String> {
138    if let Some(f) = filter {
139        // 条件拒绝名单(缺陷 D-02)
140        validate_condition(f)?;
141    }
142    let schema = registry.get(schema_name)?;
143    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
144    if let Some(f) = filter {
145        validate_condition_shape(schema, f, registry.profile())?;
146        // §9.6 关系聚合谓词无法用标量 count 表达 → 显式 Err(与 query_with_count 同口径)
147        if has_relation_predicate(schema, f) {
148            return Err(
149                "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
150                 请改用 query 并在调用方自行统计"
151                    .to_string(),
152            );
153        }
154    }
155    let base = match filter {
156        None | Some(Value::Null) => json!({}),
157        Some(v) => v.clone(),
158    };
159    // 静态 owner 条件叠加 RBAC 行条件(两引擎 $and,deny-wins)
160    let owner_merged = merge_owner_condition(registry.role_rules(), schema, ctx, Some(base));
161    let filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)
162        .unwrap_or_else(|| json!({}));
163    Ok(cmd_count_documents(schema, &filter))
164}
165
166pub(super) fn has_creator_permission(schema: &Schema) -> bool {
167    let has = |list: &Option<Vec<String>>| {
168        list.as_ref()
169            .map(|l| l.iter().any(|r| r == "creator"))
170            .unwrap_or(false)
171    };
172    has(&schema.read) || has(&schema.write)
173}
174
175// ─── 写权限探针(对应 JS `_checkWritePerm`) ─────────────────
176
177/// creator 写权限的探针状态:
178/// `NotProbed` → 尚未探查(可能返回需要探针的命令)
179/// `NoResult` → 探针无结果(拒绝)
180/// `Found`    → 探针命中的 `{_id, createdBy}` 文档
181#[derive(Debug, Clone, Copy)]
182pub enum Probe<'a> {
183    NotProbed,
184    NoResult,
185    Found(&'a Value),
186}
187
188/// Schema 级写权限检查(对应 JS `_checkWritePerm`):
189/// 拒写清单命中直接拒绝;非写授权时仅 creator 命中才放行(需 Host 先执行探针命令)。
190///
191/// RBAC 叠加(deny-wins):
192/// - 表级:`ensure_write(action)` 在 guest 检查后判定,deny 即拒;
193/// - 行级:`row_condition` 非空(ownerOnly / condition)时**同样需要探针**——
194///   静态放行的路径也要先取 `{_id, createdBy, condition 键}` 再重入判定
195///   ([`ensure_write_on_doc`]),杜绝 ownerOnly 单条更新静默命中他人文档;
196/// - 探针重入(`Probe::Found`)时静态 `evaluate` 与 RBAC 行级判定都过才放行。
197///
198/// 返回:`Ok(None)` = 放行;`Ok(Some(cmd))` = Host 先执行该 findOne 探针后携
199/// [`Probe::Found`] / [`Probe::NoResult`] 重入;`Err` = 拒绝(Host 映射 PermissionError)。
200#[allow(clippy::too_many_arguments)]
201pub fn check_write_perm(
202    registry: &Registry,
203    schema: &Schema,
204    ctx: Option<&Context>,
205    condition: &Value,
206    deny_msg: &str,
207    probe: Probe,
208    action: WriteAction,
209) -> Result<Option<Value>, String> {
210    // JS:`if (ctx)` 才做检查 —— 无上下文(内部调用)不设防
211    let Some(c) = ctx else {
212        return Ok(None);
213    };
214    // 拒写清单(默认空——无拒写;原 guest 硬编码随清单化移除,设计 §11.4)
215    let rules = registry.role_rules();
216    if crate::permission::has_any_role(c, &rules.deny_write_roles) {
217        return Err(deny_msg.to_string());
218    }
219    // RBAC 表级写判定(deny-wins;拒绝文案自带 RBAC 标识,不走入参 deny_msg)
220    ensure_write(registry, schema, Some(c), action)?;
221    // RBAC 行级限制是否存在(存在则探针投影需并入 condition 字段键)
222    let rbac_row_restricted =
223        crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
224
225    let probe_projection = |extra: &[String]| -> Value {
226        let mut m = Map::new();
227        m.insert("_id".to_string(), json!(1));
228        m.insert("createdBy".to_string(), json!(1));
229        for f in extra {
230            m.insert(f.clone(), json!(1));
231        }
232        Value::Object(m)
233    };
234    let rbac_fields = probe_condition_fields(registry, schema, Some(c), action.as_str());
235
236    if can_write_schema(registry.role_rules(), schema, Some(c)) {
237        // 静态放行:RBAC 行级受限时仍需探针(否则静默放行越权行更新/删除)
238        if rbac_row_restricted && is_truthy(condition) {
239            return match probe {
240                Probe::NotProbed => Ok(Some(cmd_find_one(
241                    schema,
242                    condition,
243                    Some(&probe_projection(&rbac_fields)),
244                ))),
245                Probe::NoResult => Err(deny_msg.to_string()),
246                Probe::Found(doc) => {
247                    ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
248                }
249            };
250        }
251        return Ok(None);
252    }
253    let creator_only = schema
254        .write
255        .as_ref()
256        .map(|w| w.iter().any(|r| r == "creator"))
257        .unwrap_or(false);
258    if creator_only && is_truthy(condition) {
259        let needs_row_check = rbac_row_restricted;
260        return match probe {
261            Probe::NotProbed => Ok(Some(cmd_find_one(
262                schema,
263                condition,
264                Some(&probe_projection(&rbac_fields)),
265            ))),
266            Probe::NoResult => Err(deny_msg.to_string()),
267                Probe::Found(doc) => {
268                    if evaluate(
269                        registry.role_rules(),
270                        Some(c),
271                        schema.write.as_deref(),
272                        Doc::Doc(doc),
273                    ) {
274                        if needs_row_check {
275                            // 静态 creator 过 → RBAC 行级再判(两引擎都过才放行)
276                            ensure_write_on_doc(registry, schema, Some(c), action, doc)
277                                .map(|_| None)
278                        } else {
279                            Ok(None)
280                        }
281                    } else {
282                        Err(deny_msg.to_string())
283                    }
284                }
285        };
286    }
287    Err(deny_msg.to_string())
288}