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_is_autoincrement() {
67            // 阶段2:数据库自增主键 —— doc 不注入 `_id`(SQL INSERT 列白名单来自 doc 键,
68            // 无 `_id` 列即由数据库赋值;PG/SQLite 走 RETURNING 回读、MySQL 走宿主
69            // lastrowid)。Mongo 无自增语义,由宿主 Mongo 执行器对此形态显式报错
70            // (AUTOINCREMENT_NOT_SUPPORTED),禁 ObjectId 静默顶替。
71        } else if !schema.id_prefix.is_empty() {
72            doc.insert("_id".to_string(), json!(new_id));
73        } else {
74            return Err(format!(
75                "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
76                schema.name
77            ));
78        }
79    }
80
81    // 自动设置 createdBy(creator 权限场景)
82    if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
83        if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
84            doc.insert("createdBy".to_string(), json!(uid));
85        }
86    }
87
88    // 自动时间戳
89    if schema.timestamps {
90        if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
91            doc.insert("createdAt".to_string(), json!(now));
92        }
93        doc.insert("updatedAt".to_string(), json!(now));
94    }
95    Ok(doc)
96}
97
98/// 判断存在性(对应 JS `exists`)
99pub fn plan_exists(
100    schema_name: &str,
101    registry: &Registry,
102    condition: &Value,
103) -> Result<Value, String> {
104    // 条件拒绝名单(缺陷 D-02)
105    validate_condition(condition)?;
106    let schema = registry.get(schema_name)?;
107    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
108    validate_condition_shape(schema, condition, registry.profile())?;
109    // §9.6 关系聚合谓词无法用标量 `findOne` 表达 → 显式 Err(否则 Mongo 静默给错结果)
110    if has_relation_predicate(schema, condition) {
111        return Err(
112            "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
113             请改用 query 并自行判断是否有结果"
114                .to_string(),
115        );
116    }
117    Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
118}
119
120/// 统计数量(对应 JS `count`):filter 为 nullish 时用 `{}`
121///
122/// R1:owner(read=creator) 场景下,count 必须注入 `createdBy = ctx.userId`,
123/// 否则非 admin 用户 count 会越权统计全表(E-08)。
124pub fn plan_count(
125    schema_name: &str,
126    registry: &Registry,
127    filter: Option<&Value>,
128    ctx: Option<&Context>,
129) -> Result<Value, String> {
130    if let Some(f) = filter {
131        // 条件拒绝名单(缺陷 D-02)
132        validate_condition(f)?;
133    }
134    let schema = registry.get(schema_name)?;
135    // §11.4(D2):与 pipeline 读路径同码拒绝 U1~U4 形态(数组/对象/点号路径)
136    if let Some(f) = filter {
137        validate_condition_shape(schema, f, registry.profile())?;
138        // §9.6 关系聚合谓词无法用标量 count 表达 → 显式 Err(与 query_with_count 同口径)
139        if has_relation_predicate(schema, f) {
140            return Err(
141                "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
142                 请改用 query 并在调用方自行统计"
143                    .to_string(),
144            );
145        }
146    }
147    let base = match filter {
148        None | Some(Value::Null) => json!({}),
149        Some(v) => v.clone(),
150    };
151    let filter = merge_owner_condition(schema, ctx, Some(base)).unwrap_or_else(|| json!({}));
152    Ok(cmd_count_documents(schema, &filter))
153}
154
155pub(super) fn has_creator_permission(schema: &Schema) -> bool {
156    let has = |list: &Option<Vec<String>>| {
157        list.as_ref()
158            .map(|l| l.iter().any(|r| r == "creator"))
159            .unwrap_or(false)
160    };
161    has(&schema.read) || has(&schema.write)
162}
163
164// ─── 写权限探针(对应 JS `_checkWritePerm`) ─────────────────
165
166/// creator 写权限的探针状态:
167/// `NotProbed` → 尚未探查(可能返回需要探针的命令)
168/// `NoResult` → 探针无结果(拒绝)
169/// `Found`    → 探针命中的 `{_id, createdBy}` 文档
170#[derive(Debug, Clone, Copy)]
171pub enum Probe<'a> {
172    NotProbed,
173    NoResult,
174    Found(&'a Value),
175}
176
177/// Schema 级写权限检查(对应 JS `_checkWritePerm`):
178/// guest 直接拒绝;非写授权时仅 creator 命中才放行(需 Host 先执行探针命令)。
179///
180/// 返回:`Ok(None)` = 放行;`Ok(Some(cmd))` = Host 先执行该 findOne 探针后携
181/// [`Probe::Found`] / [`Probe::NoResult`] 重入;`Err` = 拒绝(Host 映射 PermissionError)。
182pub fn check_write_perm(
183    schema: &Schema,
184    ctx: Option<&Context>,
185    condition: &Value,
186    deny_msg: &str,
187    probe: Probe,
188) -> Result<Option<Value>, String> {
189    // JS:`if (ctx)` 才做检查 —— 无上下文(内部调用)不设防
190    let Some(c) = ctx else {
191        return Ok(None);
192    };
193    if c.roles
194        .clone()
195        .unwrap_or_default()
196        .iter()
197        .any(|r| r == "guest")
198    {
199        return Err(deny_msg.to_string());
200    }
201    if can_write_schema(schema, Some(c)) {
202        return Ok(None);
203    }
204    let creator_only = schema
205        .write
206        .as_ref()
207        .map(|w| w.iter().any(|r| r == "creator"))
208        .unwrap_or(false);
209    if creator_only && is_truthy(condition) {
210        return match probe {
211            Probe::NotProbed => Ok(Some(cmd_find_one(
212                schema,
213                condition,
214                Some(&json!({ "_id": 1, "createdBy": 1 })),
215            ))),
216            Probe::NoResult => Err(deny_msg.to_string()),
217            Probe::Found(doc) => {
218                if evaluate(Some(c), schema.write.as_deref(), Doc::Doc(doc)) {
219                    Ok(None)
220                } else {
221                    Err(deny_msg.to_string())
222                }
223            }
224        };
225    }
226    Err(deny_msg.to_string())
227}