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(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(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 if c.roles
215 .clone()
216 .unwrap_or_default()
217 .iter()
218 .any(|r| r == "guest")
219 {
220 return Err(deny_msg.to_string());
221 }
222 ensure_write(registry, schema, Some(c), action)?;
224 let rbac_row_restricted =
226 crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
227
228 let probe_projection = |extra: &[String]| -> Value {
229 let mut m = Map::new();
230 m.insert("_id".to_string(), json!(1));
231 m.insert("createdBy".to_string(), json!(1));
232 for f in extra {
233 m.insert(f.clone(), json!(1));
234 }
235 Value::Object(m)
236 };
237 let rbac_fields = probe_condition_fields(registry, schema, Some(c), action.as_str());
238
239 if can_write_schema(schema, Some(c)) {
240 if rbac_row_restricted && is_truthy(condition) {
242 return match probe {
243 Probe::NotProbed => Ok(Some(cmd_find_one(
244 schema,
245 condition,
246 Some(&probe_projection(&rbac_fields)),
247 ))),
248 Probe::NoResult => Err(deny_msg.to_string()),
249 Probe::Found(doc) => {
250 ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
251 }
252 };
253 }
254 return Ok(None);
255 }
256 let creator_only = schema
257 .write
258 .as_ref()
259 .map(|w| w.iter().any(|r| r == "creator"))
260 .unwrap_or(false);
261 if creator_only && is_truthy(condition) {
262 let needs_row_check = rbac_row_restricted;
263 return match probe {
264 Probe::NotProbed => Ok(Some(cmd_find_one(
265 schema,
266 condition,
267 Some(&probe_projection(&rbac_fields)),
268 ))),
269 Probe::NoResult => Err(deny_msg.to_string()),
270 Probe::Found(doc) => {
271 if evaluate(Some(c), schema.write.as_deref(), Doc::Doc(doc)) {
272 if needs_row_check {
273 ensure_write_on_doc(registry, schema, Some(c), action, doc)
275 .map(|_| None)
276 } else {
277 Ok(None)
278 }
279 } else {
280 Err(deny_msg.to_string())
281 }
282 }
283 };
284 }
285 Err(deny_msg.to_string())
286}