1use 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
21pub 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 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
50pub(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 Some(_) => filter_writable_data_overlay(registry, schema, ctx, data),
63 None => data.clone(),
64 };
65
66 let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
69
70 if !doc.get("_id").map(is_truthy).unwrap_or(false) {
74 if schema.id_is_autoincrement() {
75 } 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 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 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
106pub fn plan_exists(
108 schema_name: &str,
109 registry: &Registry,
110 condition: &Value,
111) -> Result<Value, String> {
112 validate_condition(condition)?;
114 let schema = registry.get(schema_name)?;
115 validate_condition_shape(schema, condition, registry.profile())?;
117 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
128pub 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 validate_condition(f)?;
141 }
142 let schema = registry.get(schema_name)?;
143 if let Some(f) = filter {
145 validate_condition_shape(schema, f, registry.profile())?;
146 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 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#[derive(Debug, Clone, Copy)]
182pub enum Probe<'a> {
183 NotProbed,
184 NoResult,
185 Found(&'a Value),
186}
187
188#[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 let Some(c) = ctx else {
212 return Ok(None);
213 };
214 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 ensure_write(registry, schema, Some(c), action)?;
221 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 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 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}