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, filter_writable_data, merge_owner_condition, Context, Doc,
8};
9use crate::schema::{Registry, Schema};
10use crate::types::{
11    has_relation_predicate, is_truthy, validate_condition, validate_condition_shape,
12};
13
14use super::cmd::{cmd_count_documents, cmd_find_one, cmd_insert_one};
15use super::{ensure_context, ERR_NO_WRITE};
16
17/// 生成插入命令(对应 JS `insert`)
18///
19/// `now` 与 `new_id` 由 Host 提供(core 无时钟与随机源,且这样可让产出确定、便于对拍)。
20pub fn plan_insert(
21    schema_name: &str,
22    registry: &Registry,
23    ctx: Option<&Context>,
24    data: &Value,
25    now: i64,
26    new_id: &str,
27    fn_registry: Option<&dyn FnRegistry>,
28) -> Result<Value, String> {
29    ensure_context(registry, ctx)?;
30    let schema = registry.get(schema_name)?;
31
32    if !can_write_schema(schema, ctx) {
33        return Err(ERR_NO_WRITE.to_string());
34    }
35
36    let doc = build_insert_doc(schema, ctx, data, now, new_id)?;
37    let doc = Value::Object(doc);
38    Ok(json!({
39        "command": cmd_insert_one(schema, &doc),
40        "returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
41    }))
42}
43
44/// insert 文档规范化(对应 JS `insert` 主体):
45/// 权限过滤 → 保留显式 null → 自动 _id / createdBy / 时间戳
46pub(super) fn build_insert_doc(
47    schema: &Schema,
48    ctx: Option<&Context>,
49    data: &Value,
50    now: i64,
51    new_id: &str,
52) -> Result<Map<String, Value>, String> {
53    let filtered = match ctx {
54        Some(c) => filter_writable_data(schema, Some(c), data),
55        None => data.clone(),
56    };
57
58    // 保留显式 null:历史实现把 null 一律剔除(当成缺失),导致「空串/NULL/缺失」三态
59    // 无法区分(F-07),null 字段读回也被吞(H-01)。显式 `null` 应原样落库,SQL 落 NULL。
60    let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
61
62    // 自动生成 ID(仅在 schema 配了 idPrefix 且未提供有效 _id 时);
63    // 无 idPrefix 且无 _id → 显式报错(缺陷 D-03):静默产出「无 _id 文档」会让
64    // Host 返回 _id=undefined,且 Mongo 自动 ObjectId 会触发跨绑定序列化崩溃
65    if !doc.get("_id").map(is_truthy).unwrap_or(false) {
66        if !schema.id_prefix.is_empty() {
67            doc.insert("_id".to_string(), json!(new_id));
68        } else {
69            return Err(format!(
70                "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
71                schema.name
72            ));
73        }
74    }
75
76    // 自动设置 createdBy(creator 权限场景)
77    if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
78        if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
79            doc.insert("createdBy".to_string(), json!(uid));
80        }
81    }
82
83    // 自动时间戳
84    if schema.timestamps {
85        if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
86            doc.insert("createdAt".to_string(), json!(now));
87        }
88        doc.insert("updatedAt".to_string(), json!(now));
89    }
90    Ok(doc)
91}
92
93/// 判断存在性(对应 JS `exists`)
94pub fn plan_exists(
95    schema_name: &str,
96    registry: &Registry,
97    condition: &Value,
98) -> Result<Value, String> {
99    // 条件拒绝名单(缺陷 D-02)
100    validate_condition(condition)?;
101    let schema = registry.get(schema_name)?;
102    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
103    validate_condition_shape(schema, condition, registry.profile())?;
104    // §9.6 关系聚合谓词无法用标量 `findOne` 表达 → 显式 Err(否则 Mongo 静默给错结果)
105    if has_relation_predicate(schema, condition) {
106        return Err(
107            "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
108             请改用 query 并自行判断是否有结果"
109                .to_string(),
110        );
111    }
112    Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
113}
114
115/// 统计数量(对应 JS `count`):filter 为 nullish 时用 `{}`
116///
117/// R1:owner(read=creator) 场景下,count 必须注入 `createdBy = ctx.userId`,
118/// 否则非 admin 用户 count 会越权统计全表(E-08)。
119pub fn plan_count(
120    schema_name: &str,
121    registry: &Registry,
122    filter: Option<&Value>,
123    ctx: Option<&Context>,
124) -> Result<Value, String> {
125    if let Some(f) = filter {
126        // 条件拒绝名单(缺陷 D-02)
127        validate_condition(f)?;
128    }
129    let schema = registry.get(schema_name)?;
130    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
131    if let Some(f) = filter {
132        validate_condition_shape(schema, f, registry.profile())?;
133        // §9.6 关系聚合谓词无法用标量 count 表达 → 显式 Err(与 query_with_count 同口径)
134        if has_relation_predicate(schema, f) {
135            return Err(
136                "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
137                 请改用 query 并在调用方自行统计"
138                    .to_string(),
139            );
140        }
141    }
142    let base = match filter {
143        None | Some(Value::Null) => json!({}),
144        Some(v) => v.clone(),
145    };
146    let filter = merge_owner_condition(schema, ctx, Some(base)).unwrap_or_else(|| json!({}));
147    Ok(cmd_count_documents(schema, &filter))
148}
149
150pub(super) fn has_creator_permission(schema: &Schema) -> bool {
151    let has = |list: &Option<Vec<String>>| {
152        list.as_ref()
153            .map(|l| l.iter().any(|r| r == "creator"))
154            .unwrap_or(false)
155    };
156    has(&schema.read) || has(&schema.write)
157}
158
159// ─── 写权限探针(对应 JS `_checkWritePerm`) ─────────────────
160
161/// creator 写权限的探针状态:
162/// `NotProbed` → 尚未探查(可能返回需要探针的命令)
163/// `NoResult` → 探针无结果(拒绝)
164/// `Found`    → 探针命中的 `{_id, createdBy}` 文档
165#[derive(Debug, Clone, Copy)]
166pub enum Probe<'a> {
167    NotProbed,
168    NoResult,
169    Found(&'a Value),
170}
171
172/// Schema 级写权限检查(对应 JS `_checkWritePerm`):
173/// guest 直接拒绝;非写授权时仅 creator 命中才放行(需 Host 先执行探针命令)。
174///
175/// 返回:`Ok(None)` = 放行;`Ok(Some(cmd))` = Host 先执行该 findOne 探针后携
176/// [`Probe::Found`] / [`Probe::NoResult`] 重入;`Err` = 拒绝(Host 映射 PermissionError)。
177pub fn check_write_perm(
178    schema: &Schema,
179    ctx: Option<&Context>,
180    condition: &Value,
181    deny_msg: &str,
182    probe: Probe,
183) -> Result<Option<Value>, String> {
184    // JS:`if (ctx)` 才做检查 —— 无上下文(内部调用)不设防
185    let Some(c) = ctx else {
186        return Ok(None);
187    };
188    if c.roles
189        .clone()
190        .unwrap_or_default()
191        .iter()
192        .any(|r| r == "guest")
193    {
194        return Err(deny_msg.to_string());
195    }
196    if can_write_schema(schema, Some(c)) {
197        return Ok(None);
198    }
199    let creator_only = schema
200        .write
201        .as_ref()
202        .map(|w| w.iter().any(|r| r == "creator"))
203        .unwrap_or(false);
204    if creator_only && is_truthy(condition) {
205        return match probe {
206            Probe::NotProbed => Ok(Some(cmd_find_one(
207                schema,
208                condition,
209                Some(&json!({ "_id": 1, "createdBy": 1 })),
210            ))),
211            Probe::NoResult => Err(deny_msg.to_string()),
212            Probe::Found(doc) => {
213                if evaluate(Some(c), schema.write.as_deref(), Doc::Doc(doc)) {
214                    Ok(None)
215                } else {
216                    Err(deny_msg.to_string())
217                }
218            }
219        };
220    }
221    Err(deny_msg.to_string())
222}