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::write_links::attach_write_links;
18use super::{ensure_context, ERR_NO_WRITE};
19
20pub fn plan_insert(
24 schema_name: &str,
25 registry: &Registry,
26 ctx: Option<&Context>,
27 data: &Value,
28 now: i64,
29 new_id: &str,
30 fn_registry: Option<&dyn FnRegistry>,
31) -> Result<Value, String> {
32 ensure_context(registry, ctx)?;
33 let schema = registry.get(schema_name)?;
34
35 if !can_write_schema(registry.role_rules(), schema, ctx) {
36 return Err(ERR_NO_WRITE.to_string());
37 }
38 ensure_write(registry, schema, ctx, WriteAction::Insert)?;
40
41 let doc = build_insert_doc(registry, schema, ctx, data, now, new_id)?;
42 let doc = Value::Object(doc);
43 let plan = json!({
44 "command": cmd_insert_one(schema, &doc),
45 "returns": apply_defaults_and_computes(&doc, schema, fn_registry)?,
46 });
47 attach_write_links(plan, registry, &[schema_name], registry.write_link_policy())
49}
50
51pub(super) fn build_insert_doc(
54 registry: &Registry,
55 schema: &Schema,
56 ctx: Option<&Context>,
57 data: &Value,
58 now: i64,
59 new_id: &str,
60) -> Result<Map<String, Value>, String> {
61 let filtered = match ctx {
62 Some(_) => filter_writable_data_overlay(registry, schema, ctx, data),
64 None => data.clone(),
65 };
66
67 let mut doc: Map<String, Value> = filtered.as_object().cloned().unwrap_or_default();
70
71 if !doc.get("_id").map(is_truthy).unwrap_or(false) {
75 if schema.id_is_autoincrement() {
76 } else if !schema.id_prefix.is_empty() {
81 doc.insert("_id".to_string(), json!(new_id));
82 } else {
83 return Err(format!(
84 "schema \"{}\" 未配置 idPrefix 且未提供有效 _id:请显式提供 _id,或为 schema 配置 idPrefix",
85 schema.name
86 ));
87 }
88 }
89
90 if has_creator_permission(schema) && !doc.get("createdBy").map(is_truthy).unwrap_or(false) {
92 if let Some(uid) = ctx.and_then(|c| c.user_id.clone()) {
93 doc.insert("createdBy".to_string(), json!(uid));
94 }
95 }
96
97 if schema.timestamps {
99 if !doc.get("createdAt").map(is_truthy).unwrap_or(false) {
100 doc.insert("createdAt".to_string(), json!(now));
101 }
102 doc.insert("updatedAt".to_string(), json!(now));
103 }
104 Ok(doc)
105}
106
107pub fn plan_exists(
109 schema_name: &str,
110 registry: &Registry,
111 condition: &Value,
112) -> Result<Value, String> {
113 validate_condition(condition)?;
115 let schema = registry.get(schema_name)?;
116 validate_condition_shape(schema, condition, registry.profile())?;
118 if has_relation_predicate(schema, condition) {
120 return Err(
121 "exists 不支持关系聚合谓词条件(§9.6):标量查询无法表达关系谓词,\
122 请改用 query 并自行判断是否有结果"
123 .to_string(),
124 );
125 }
126 Ok(cmd_find_one(schema, condition, Some(&json!({ "_id": 1 }))))
127}
128
129pub fn plan_count(
134 schema_name: &str,
135 registry: &Registry,
136 filter: Option<&Value>,
137 ctx: Option<&Context>,
138) -> Result<Value, String> {
139 if let Some(f) = filter {
140 validate_condition(f)?;
142 }
143 let schema = registry.get(schema_name)?;
144 if let Some(f) = filter {
146 validate_condition_shape(schema, f, registry.profile())?;
147 if has_relation_predicate(schema, f) {
149 return Err(
150 "count 不支持关系聚合谓词条件(§9.6):标量计数无法表达关系谓词,\
151 请改用 query 并在调用方自行统计"
152 .to_string(),
153 );
154 }
155 }
156 let base = match filter {
157 None | Some(Value::Null) => json!({}),
158 Some(v) => v.clone(),
159 };
160 let owner_merged = merge_owner_condition(registry.role_rules(), schema, ctx, Some(base));
162 let filter = merge_row_condition(registry, schema, ctx, "read", owner_merged)
163 .unwrap_or_else(|| json!({}));
164 Ok(cmd_count_documents(schema, &filter))
165}
166
167pub(super) fn has_creator_permission(schema: &Schema) -> bool {
168 let has = |list: &Option<Vec<String>>| {
169 list.as_ref()
170 .map(|l| l.iter().any(|r| r == "creator"))
171 .unwrap_or(false)
172 };
173 has(&schema.read) || has(&schema.write)
174}
175
176#[derive(Debug, Clone, Copy)]
183pub enum Probe<'a> {
184 NotProbed,
185 NoResult,
186 Found(&'a Value),
187}
188
189#[allow(clippy::too_many_arguments)]
202pub fn check_write_perm(
203 registry: &Registry,
204 schema: &Schema,
205 ctx: Option<&Context>,
206 condition: &Value,
207 deny_msg: &str,
208 probe: Probe,
209 action: WriteAction,
210) -> Result<Option<Value>, String> {
211 let Some(c) = ctx else {
213 return Ok(None);
214 };
215 let rules = registry.role_rules();
217 if crate::permission::has_any_role(c, &rules.deny_write_roles) {
218 return Err(deny_msg.to_string());
219 }
220 ensure_write(registry, schema, Some(c), action)?;
222 let rbac_row_restricted =
224 crate::rbac::row_condition(registry, schema, Some(c), action.as_str()).is_some();
225
226 let probe_projection = |extra: &[String]| -> Value {
227 let mut m = Map::new();
228 m.insert("_id".to_string(), json!(1));
229 m.insert("createdBy".to_string(), json!(1));
230 for f in extra {
231 m.insert(f.clone(), json!(1));
232 }
233 Value::Object(m)
234 };
235 let rbac_fields = probe_condition_fields(registry, schema, Some(c), action.as_str());
236
237 if can_write_schema(registry.role_rules(), schema, Some(c)) {
238 if rbac_row_restricted && is_truthy(condition) {
240 return match probe {
241 Probe::NotProbed => Ok(Some(cmd_find_one(
242 schema,
243 condition,
244 Some(&probe_projection(&rbac_fields)),
245 ))),
246 Probe::NoResult => Err(deny_msg.to_string()),
247 Probe::Found(doc) => {
248 ensure_write_on_doc(registry, schema, Some(c), action, doc).map(|_| None)
249 }
250 };
251 }
252 return Ok(None);
253 }
254 let creator_only = schema
255 .write
256 .as_ref()
257 .map(|w| w.iter().any(|r| r == "creator"))
258 .unwrap_or(false);
259 if creator_only && is_truthy(condition) {
260 let needs_row_check = rbac_row_restricted;
261 return match probe {
262 Probe::NotProbed => Ok(Some(cmd_find_one(
263 schema,
264 condition,
265 Some(&probe_projection(&rbac_fields)),
266 ))),
267 Probe::NoResult => Err(deny_msg.to_string()),
268 Probe::Found(doc) => {
269 if evaluate(
270 registry.role_rules(),
271 Some(c),
272 schema.write.as_deref(),
273 Doc::Doc(doc),
274 ) {
275 if needs_row_check {
276 ensure_write_on_doc(registry, schema, Some(c), action, doc).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}