1use serde_json::{json, Map, Value};
4
5use crate::computes::{apply_defaults_and_computes, FnRegistry};
6use crate::permission::{can_write_schema, evaluate, merge_owner_condition, Context, Doc};
7use crate::rbac::{
8 ensure_write, ensure_write_on_doc, filter_writable_data_overlay, merge_row_condition,
9 probe_condition_fields, WriteAction,
10};
11use crate::schema::{Registry, Schema};
12use crate::types::{
13 has_relation_predicate, is_truthy, validate_condition, validate_condition_shape,
14};
15
16use super::cmd::{cmd_count_documents, cmd_find_one, cmd_insert_one};
17use super::{ensure_context, ERR_NO_WRITE};
18
19pub fn plan_insert(
23 schema_name: &str,
24 registry: &Registry,
25 ctx: Option<&Context>,
26 data: &Value,
27 now: i64,
28 new_id: &str,
29 fn_registry: Option<&dyn FnRegistry>,
30) -> Result<Value, String> {
31 ensure_context(registry, ctx)?;
32 let schema = registry.get(schema_name)?;
33
34 if !can_write_schema(registry.role_rules(), schema, ctx) {
35 return Err(ERR_NO_WRITE.to_string());
36 }
37 ensure_write(registry, schema, ctx, WriteAction::Insert)?;
39
40 let doc = build_insert_doc(registry, schema, ctx, data, now, new_id)?;
41 let doc = Value::Object(doc);
42 Ok(json!({
43 "command": cmd_insert_one(schema, &doc),
44 "returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
45 }))
46}
47
48pub(super) fn build_insert_doc(
51 registry: &Registry,
52 schema: &Schema,
53 ctx: Option<&Context>,
54 data: &Value,
55 now: i64,
56 new_id: &str,
57) -> Result<Map<String, Value>, String> {
58 let filtered = match ctx {
59 Some(_) => filter_writable_data_overlay(registry, schema, ctx, data),
61 None => data.clone(),
62 };
63
64 let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
67
68 if !doc.get("_id").map(is_truthy).unwrap_or(false) {
72 if schema.id_is_autoincrement() {
73 } else if !schema.id_prefix.is_empty() {
78 doc.insert("_id".to_string(), json!(new_id));
79 } else {
80 return Err(format!(
81 "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
82 schema.name
83 ));
84 }
85 }
86
87 if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
89 if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
90 doc.insert("createdBy".to_string(), json!(uid));
91 }
92 }
93
94 if schema.timestamps {
96 if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
97 doc.insert("createdAt".to_string(), json!(now));
98 }
99 doc.insert("updatedAt".to_string(), json!(now));
100 }
101 Ok(doc)
102}
103
104pub fn plan_exists(
106 schema_name: &str,
107 registry: &Registry,
108 condition: &Value,
109) -> Result<Value, String> {
110 validate_condition(condition)?;
112 let schema = registry.get(schema_name)?;
113 validate_condition_shape(schema, condition, registry.profile())?;
115 if has_relation_predicate(schema, condition) {
117 return Err(
118 "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
119 请改用 query 并自行判断是否有结果"
120 .to_string(),
121 );
122 }
123 Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
124}
125
126pub fn plan_count(
131 schema_name: &str,
132 registry: &Registry,
133 filter: Option<&Value>,
134 ctx: Option<&Context>,
135) -> Result<Value, String> {
136 if let Some(f) = filter {
137 validate_condition(f)?;
139 }
140 let schema = registry.get(schema_name)?;
141 if let Some(f) = filter {
143 validate_condition_shape(schema, f, registry.profile())?;
144 if has_relation_predicate(schema, f) {
146 return Err(
147 "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
148 请改用 query 并在调用方自行统计"
149 .to_string(),
150 );
151 }
152 }
153 let base = match filter {
154 None | Some(Value::Null) => json!({}),
155 Some(v) => v.clone(),
156 };
157 let owner_merged = merge_owner_condition(registry.role_rules(), schema, ctx, Some(base));
159 let filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)
160 .unwrap_or_else(|| json!({}));
161 Ok(cmd_count_documents(schema, &filter))
162}
163
164pub(super) fn has_creator_permission(schema: &Schema) -> bool {
165 let has = |list: &Option<Vec<String>>| {
166 list.as_ref()
167 .map(|l| l.iter().any(|r| r == "creator"))
168 .unwrap_or(false)
169 };
170 has(&schema.read) || has(&schema.write)
171}
172
173#[derive(Debug, Clone, Copy)]
180pub enum Probe<'a> {
181 NotProbed,
182 NoResult,
183 Found(&'a Value),
184}
185
186#[allow(clippy::too_many_arguments)]
199pub fn check_write_perm(
200 registry: &Registry,
201 schema: &Schema,
202 ctx: Option<&Context>,
203 condition: &Value,
204 deny_msg: &str,
205 probe: Probe,
206 action: WriteAction,
207) -> Result<Option<Value>, String> {
208 let Some(c) = ctx else {
210 return Ok(None);
211 };
212 let rules = registry.role_rules();
214 if crate::permission::has_any_role(c, &rules.deny_write_roles) {
215 return Err(deny_msg.to_string());
216 }
217 ensure_write(registry, schema, Some(c), action)?;
219 let rbac_row_restricted =
221 crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
222
223 let probe_projection = |extra: &[String]| -> Value {
224 let mut m = Map::new();
225 m.insert("_id".to_string(), json!(1));
226 m.insert("createdBy".to_string(), json!(1));
227 for f in extra {
228 m.insert(f.clone(), json!(1));
229 }
230 Value::Object(m)
231 };
232 let rbac_fields = probe_condition_fields(registry, schema, Some(c), action.as_str());
233
234 if can_write_schema(registry.role_rules(), schema, Some(c)) {
235 if rbac_row_restricted && is_truthy(condition) {
237 return match probe {
238 Probe::NotProbed => Ok(Some(cmd_find_one(
239 schema,
240 condition,
241 Some(&probe_projection(&rbac_fields)),
242 ))),
243 Probe::NoResult => Err(deny_msg.to_string()),
244 Probe::Found(doc) => {
245 ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
246 }
247 };
248 }
249 return Ok(None);
250 }
251 let creator_only = schema
252 .write
253 .as_ref()
254 .map(|w| w.iter().any(|r| r == "creator"))
255 .unwrap_or(false);
256 if creator_only && is_truthy(condition) {
257 let needs_row_check = rbac_row_restricted;
258 return match probe {
259 Probe::NotProbed => Ok(Some(cmd_find_one(
260 schema,
261 condition,
262 Some(&probe_projection(&rbac_fields)),
263 ))),
264 Probe::NoResult => Err(deny_msg.to_string()),
265 Probe::Found(doc) => {
266 if evaluate(
267 registry.role_rules(),
268 Some(c),
269 schema.write.as_deref(),
270 Doc::Doc(doc),
271 ) {
272 if needs_row_check {
273 ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
275 } else {
276 Ok(None)
277 }
278 } else {
279 Err(deny_msg.to_string())
280 }
281 }
282 };
283 }
284 Err(deny_msg.to_string())
285}