1use serde_json::{json, Map, Value};
15
16use super::expressions::{literal_type, row_of, schema_type};
17use super::package::PackageIssue;
18use super::records::{validate_record, validate_record_schema};
19use super::utf16_cmp;
20use crate::cel::{check_expression, effect_words, evaluate_counted, is_optional, optional_type, parse_expression, required_type, type_display, type_name, ExpressionEnv, FunctionEffect, MAX_STEPS};
21
22pub use crate::cel::function_effect;
23
24pub const ACTION_LIMIT_ACTIONS: usize = 32;
27pub const ACTION_LIMIT_STEPS: usize = 64;
28pub const ACTION_LIMIT_MAX_STEPS: usize = 100_000;
29pub const ACTION_STEP_KINDS: &[&str] = &["require", "records.upsert", "records.delete", "set", "call"];
31pub const ACTION_NAME_PATTERN: &str = "^[a-z][A-Za-z0-9_]{0,31}$";
33pub const ACTION_CALL_PREFIX: &str = "actions.";
35pub const RECORDS_BATCH_FUNCTION: &str = "records.batch";
37
38#[derive(Default, Clone, Copy)]
40pub struct ActionContext<'a> {
41 pub data: Option<&'a Value>,
43 pub me: Option<&'a str>,
45 pub now: Option<f64>,
47 pub offset: Option<&'a (dyn Fn(f64) -> i32 + Send + Sync)>,
49 pub max_steps: Option<usize>,
51}
52
53fn escape(key: &str) -> String {
54 key.replace('~', "~0").replace('/', "~1")
55}
56
57fn issue(path: impl Into<String>, message: impl Into<String>) -> PackageIssue {
58 PackageIssue { path: path.into(), message: message.into() }
59}
60
61fn ty(name: &str) -> Value {
62 Value::String(name.into())
63}
64
65fn is_pointer(path: &str) -> bool {
67 if path.is_empty() {
68 return true;
69 }
70 if !path.starts_with('/') {
71 return false;
72 }
73 let mut chars = path.chars();
74 while let Some(c) = chars.next() {
75 if c == '~' && !matches!(chars.next(), Some('0' | '1')) {
76 return false;
77 }
78 }
79 true
80}
81
82pub fn is_action_name(name: &str) -> bool {
84 let b = name.as_bytes();
85 !b.is_empty() && b.len() <= 32 && b[0].is_ascii_lowercase() && b[1..].iter().all(|c| c.is_ascii_alphanumeric() || *c == b'_')
86}
87
88pub fn action_call_name(call: &str) -> Option<&str> {
90 call.strip_prefix(ACTION_CALL_PREFIX)
91}
92
93fn step_kind(step: &Value) -> Option<&'static str> {
95 let step = step.as_object()?;
96 let keys: Vec<&'static str> = step.keys().filter_map(|k| ACTION_STEP_KINDS.iter().find(|s| **s == k.as_str()).copied()).collect();
97 if keys.len() == 1 {
98 Some(keys[0])
99 } else {
100 None
101 }
102}
103
104fn sorted_keys(m: &Map<String, Value>) -> Vec<&String> {
105 let mut keys: Vec<&String> = m.keys().collect();
106 keys.sort_by(|a, b| utf16_cmp(a, b));
107 keys
108}
109
110pub fn action_var_types(action: &Map<String, Value>) -> Map<String, Value> {
113 let schema = action.get("params").and_then(Value::as_object);
114 let empty = Map::new();
115 let props = schema.and_then(|s| s.get("properties")).and_then(Value::as_object).unwrap_or(&empty);
116 let mut types = Map::new();
117 types.insert("params".into(), row_of(props, schema.and_then(|s| s.get("required"))));
118 types.insert("data".into(), optional_type(ty("dyn")));
120 types.insert("me".into(), optional_type(ty("string")));
121 types
122}
123
124fn textual(t: &Value) -> bool {
125 matches!(t.as_str(), Some("string" | "date" | "timestamp"))
126}
127
128fn fits(field: &Value, t: &Value) -> bool {
130 if is_optional(t) {
132 return is_optional(field) && fits(&required_type(field), &required_type(t));
133 }
134 if is_optional(field) {
135 return fits(&required_type(field), t);
136 }
137 if field == "dyn" || t == "dyn" || t == "null" {
138 return true;
139 }
140 if textual(field) {
141 return textual(t);
142 }
143 type_display(field) == type_display(t)
144}
145
146pub fn validate_package_actions(pkg: &Map<String, Value>) -> Vec<PackageIssue> {
152 let mut issues = Vec::new();
153 let Some(actions) = pkg.get("actions") else { return issues };
154 let Some(actions) = actions.as_object() else {
155 issues.push(issue("/actions", "actions is an object of named actions"));
156 return issues;
157 };
158 let names = sorted_keys(actions);
159 if names.len() > ACTION_LIMIT_ACTIONS {
160 issues.push(issue("/actions", format!("at most {ACTION_LIMIT_ACTIONS} actions")));
161 }
162 for name in names {
163 issues.extend(validate_action(pkg, name, &actions[name.as_str()], &format!("/actions/{}", escape(name))));
164 }
165 issues
166}
167
168fn validate_action(pkg: &Map<String, Value>, name: &str, action: &Value, at: &str) -> Vec<PackageIssue> {
169 let mut issues = Vec::new();
170 if !is_action_name(name) {
171 issues.push(issue(at, "an action name is a letter, then letters, digits and underscores"));
172 }
173 let Some(action) = action.as_object() else {
174 issues.push(issue(at, "an action is {params?, steps}"));
175 return issues;
176 };
177 if let Some(params) = action.get("params") {
178 issues.extend(validate_record_schema(params, &format!("{at}/params")));
179 }
180 let steps = match action.get("steps").and_then(Value::as_array) {
181 Some(steps) if !steps.is_empty() => steps,
182 _ => {
183 issues.push(issue(format!("{at}/steps"), "steps is a non-empty array"));
184 return issues;
185 }
186 };
187 if steps.len() > ACTION_LIMIT_STEPS {
188 issues.push(issue(format!("{at}/steps"), format!("at most {ACTION_LIMIT_STEPS} steps")));
189 }
190 let types = action_var_types(action);
191 let empty = Map::new();
192 let records = pkg.get("records").and_then(Value::as_object).unwrap_or(&empty);
193 let functions: Option<Vec<&str>> = pkg.get("functions").and_then(Value::as_array).map(|l| l.iter().filter_map(Value::as_str).collect());
194 for (i, step) in steps.iter().enumerate() {
195 issues.extend(validate_step(step, &format!("{at}/steps/{i}"), &types, records, functions.as_deref()));
196 }
197 issues
198}
199
200fn slot(value: &Value, at: &str, types: &Map<String, Value>, issues: &mut Vec<PackageIssue>) -> Option<Value> {
203 let Value::String(source) = value else { return Some(literal_type(value)) };
204 match check_expression(source, types) {
205 Ok(t) => Some(t),
206 Err(message) => {
207 issues.push(issue(at, message));
208 None
209 }
210 }
211}
212
213fn validate_step(step: &Value, at: &str, types: &Map<String, Value>, records: &Map<String, Value>, functions: Option<&[&str]>) -> Vec<PackageIssue> {
214 let mut issues = Vec::new();
215 let (Some(kind), Some(step)) = (step_kind(step), step.as_object()) else {
216 return vec![issue(at, format!("a step is one of {}", ACTION_STEP_KINDS.join(", ")))];
217 };
218 let body = &step[kind];
219 let here = format!("{at}/{}", escape(kind));
220 match kind {
221 "require" => {
222 if body.is_string() {
223 if let Some(t) = slot(body, &here, types, &mut issues) {
224 if t != "bool" && t != "dyn" {
225 issues.push(issue(&here, format!("require needs a bool, got {}", type_display(&t))));
226 }
227 }
228 } else {
229 issues.push(issue(&here, "require is an expression"));
230 }
231 if !step.get("message").and_then(Value::as_str).is_some_and(|m| !m.is_empty()) {
232 issues.push(issue(format!("{at}/message"), "message is the text shown when the requirement fails"));
233 }
234 }
235 "records.upsert" => {
236 let Some(body) = body.as_object() else { return vec![issue(&here, "records.upsert is {kind, data, id?}")] };
237 let kind_name = body.get("kind").and_then(Value::as_str).unwrap_or("");
238 let def = records.get(kind_name).and_then(Value::as_object);
239 if def.is_none() {
240 issues.push(issue(format!("{here}/kind"), format!("records:{kind_name} names no declared kind")));
241 }
242 if let Some(id) = body.get("id") {
243 let id_at = format!("{here}/id");
244 if let Some(t) = slot(id, &id_at, types, &mut issues) {
245 if !fits(&ty("string"), &t) {
246 issues.push(issue(&id_at, format!("id is a string, got {}", type_display(&t))));
247 }
248 }
249 }
250 let Some(data) = body.get("data").and_then(Value::as_object) else {
251 issues.push(issue(format!("{here}/data"), "data is an object of field expressions"));
252 return issues;
253 };
254 let schema = def.and_then(|d| d.get("schema")).and_then(Value::as_object);
255 let empty = Map::new();
256 let props = schema.and_then(|s| s.get("properties")).and_then(Value::as_object).unwrap_or(&empty);
257 for field in sorted_keys(data) {
258 let field_at = format!("{here}/data/{}", escape(field));
259 let t = slot(&data[field.as_str()], &field_at, types, &mut issues);
260 let Some(schema) = schema else { continue };
261 let Some(prop) = props.get(field.as_str()) else {
262 if schema.get("additionalProperties") != Some(&Value::Bool(true)) {
263 issues.push(issue(&field_at, format!("{field} is not in the schema of {kind_name}")));
264 }
265 continue;
266 };
267 let required = schema.get("required").and_then(Value::as_array).is_some_and(|r| r.iter().any(|x| x.as_str() == Some(field.as_str())));
268 let want = if required { schema_type(Some(prop)) } else { optional_type(schema_type(Some(prop))) };
269 if let Some(t) = t {
270 if !fits(&want, &t) {
271 issues.push(issue(&field_at, format!("{field} is {}, got {}", type_display(&want), type_display(&t))));
272 }
273 }
274 }
275 if let (Some(schema), None) = (schema, body.get("id")) {
277 if let Some(required) = schema.get("required").and_then(Value::as_array) {
278 for field in required.iter().filter_map(Value::as_str) {
279 if !data.contains_key(field) {
280 issues.push(issue(format!("{here}/data"), format!("{field} is required by {kind_name}")));
281 }
282 }
283 }
284 }
285 }
286 "records.delete" => {
287 let Some(id) = body.as_object().and_then(|b| b.get("id")) else { return vec![issue(&here, "records.delete is {id}")] };
288 let id_at = format!("{here}/id");
289 if let Some(t) = slot(id, &id_at, types, &mut issues) {
290 if !fits(&ty("string"), &t) {
291 issues.push(issue(&id_at, format!("id is a string, got {}", type_display(&t))));
292 }
293 }
294 }
295 "set" => {
296 let body = body.as_object();
297 if !body.and_then(|b| b.get("path")).and_then(Value::as_str).is_some_and(is_pointer) {
298 issues.push(issue(format!("{here}/path"), "path is a JSON pointer"));
299 }
300 if let Some(body) = body {
301 slot(body.get("value").unwrap_or(&Value::Null), &format!("{here}/value"), types, &mut issues);
302 }
303 }
304 "call" => {
305 let Some(name) = body.as_str().filter(|n| !n.is_empty()) else { return vec![issue(&here, "call names a host function")] };
306 let effect = function_effect(name);
307 if effect == FunctionEffect::Write {
308 issues.push(issue(&here, format!("{}: use a records.upsert, records.delete or set step", effect_words(name, effect))));
309 } else if effect == FunctionEffect::Pure {
310 issues.push(issue(&here, format!("{name} is pure: compute it in an expression")));
311 } else if action_call_name(name).is_some() {
312 issues.push(issue(&here, "an action never runs another action"));
313 } else if let Some(functions) = functions.filter(|_| name != super::screens::NAVIGATE_FUNCTION) {
314 if !functions.iter().any(|p| *p == name || p.strip_suffix('*').is_some_and(|prefix| name.starts_with(prefix))) {
315 issues.push(issue(&here, format!("{name} is not in the package's functions")));
316 }
317 }
318 if let Some(args) = step.get("args") {
319 match args.as_object() {
320 None => issues.push(issue(format!("{at}/args"), "args is an object of expressions")),
321 Some(args) => {
322 for k in sorted_keys(args) {
323 slot(&args[k.as_str()], &format!("{at}/args/{}", escape(k)), types, &mut issues);
324 }
325 }
326 }
327 }
328 }
329 _ => {}
330 }
331 issues
332}
333
334fn pure(value: &Value, at: &str, issues: &mut Vec<PackageIssue>) {
335 match value {
336 Value::Array(list) => {
337 for (i, v) in list.iter().enumerate() {
338 pure(v, &format!("{at}/{i}"), issues);
339 }
340 }
341 Value::Object(obj) => {
342 if let Some(call) = obj.get("call").and_then(Value::as_str) {
343 let effect = function_effect(call);
344 if effect != FunctionEffect::Pure {
345 issues.push(issue(format!("{at}/call"), format!("{}: a binding is pure, name an action in on.press", effect_words(call, effect))));
346 }
347 }
348 for (k, v) in obj {
349 pure(v, &format!("{at}/{}", escape(k)), issues);
350 }
351 }
352 _ => {}
353 }
354}
355
356pub fn validate_package_effects(pkg: &Map<String, Value>) -> Vec<PackageIssue> {
361 let mut issues = Vec::new();
362 let Some(templates) = pkg.get("templates").and_then(Value::as_object) else { return issues };
363 let empty = Map::new();
364 let actions = pkg.get("actions").and_then(Value::as_object).unwrap_or(&empty);
365 for (id, template) in super::package::sorted_entries(templates) {
366 let Some(components) = template.as_object().and_then(|t| t.get("components")).and_then(Value::as_array) else { continue };
367 for (i, c) in components.iter().enumerate() {
368 let Some(c) = c.as_object() else { continue };
369 let at = format!("/templates/{}/components/{i}", escape(id));
370 for (k, v) in c {
371 if k != "on" {
372 pure(v, &format!("{at}/{}", escape(k)), &mut issues);
373 continue;
374 }
375 let Some(on) = v.as_object() else { continue };
376 for (event, handler) in on {
377 let hat = format!("{at}/on/{}", escape(event));
378 let Some(handler) = handler.as_object() else { continue };
379 for (hk, hv) in handler {
380 let fc = hv.as_object().filter(|_| hk == "functionCall");
381 let Some(fc) = fc else {
382 pure(hv, &format!("{hat}/{}", escape(hk)), &mut issues);
383 continue;
384 };
385 let call = fc.get("call").and_then(Value::as_str).unwrap_or("");
386 if let Some(action) = action_call_name(call) {
387 if !actions.contains_key(action) {
388 issues.push(issue(format!("{hat}/functionCall/call"), format!("action {action} names no package action")));
389 }
390 if let Some(def) = actions.get(action).and_then(Value::as_object) {
392 press_arg_issues(action, def, fc.get("args"), &format!("{hat}/functionCall/args"), &mut issues);
393 }
394 }
395 if call == "bind" {
397 bind_source_issues(pkg, fc.get("args"), &format!("{hat}/functionCall/args"), &mut issues);
398 }
399 for (fk, fv) in fc {
400 if fk != "call" {
401 pure(fv, &format!("{hat}/functionCall/{}", escape(fk)), &mut issues);
402 }
403 }
404 }
405 }
406 }
407 }
408 }
409 issues
410}
411
412fn press_arg_issues(name: &str, action: &Map<String, Value>, args: Option<&Value>, at: &str, issues: &mut Vec<PackageIssue>) {
417 if args.is_some_and(|a| !a.is_object()) {
418 issues.push(issue(at, format!("args is an object of action {name}'s params")));
419 return;
420 }
421 let empty = Map::new();
422 let schema = action.get("params").and_then(Value::as_object).unwrap_or(&empty);
423 let props = schema.get("properties").and_then(Value::as_object).unwrap_or(&empty);
424 let required: Vec<&str> = schema.get("required").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).collect();
425 let given = args.and_then(Value::as_object).unwrap_or(&empty);
426 let mut keys: Vec<&String> = given.keys().collect();
427 keys.sort();
428 for key in keys {
429 let here = format!("{at}/{}", escape(key));
430 let Some(prop) = props.get(key) else {
431 if schema.get("additionalProperties") != Some(&Value::Bool(true)) {
432 issues.push(issue(here, format!("{key} is not a param of action {name}")));
433 }
434 continue;
435 };
436 let v = &given[key];
437 if !(v.is_string() || v.is_number() || v.is_boolean()) {
438 continue;
439 }
440 let want = if required.contains(&key.as_str()) { schema_type(Some(prop)) } else { optional_type(schema_type(Some(prop))) };
441 let t = literal_type(v);
442 if !fits(&want, &t) {
443 issues.push(issue(here, format!("{key} is {}, got {}", type_display(&want), type_display(&t))));
444 }
445 }
446 for field in required {
447 if !given.contains_key(field) {
448 issues.push(issue(at, format!("{field} is required by action {name}")));
449 }
450 }
451}
452
453fn bind_source_issues(pkg: &Map<String, Value>, args: Option<&Value>, at: &str, issues: &mut Vec<PackageIssue>) {
457 let Some(source) = args.and_then(|a| a.get("source")).and_then(Value::as_str) else { return };
458 let here = format!("{at}/source");
459 let Some(parsed) = super::sources::parse_source(source) else {
460 issues.push(issue(here, "source is a scheme:name URI"));
461 return;
462 };
463 if let Some(list) = pkg.get("sources").and_then(Value::as_array).filter(|l| l.iter().all(Value::is_string)) {
464 let allowed: Vec<String> = list.iter().filter_map(Value::as_str).map(str::to_string).collect();
465 if !super::package::source_allowed(Some(&allowed), source) {
466 issues.push(issue(&here, format!("source {source} is not in the package's sources")));
467 }
468 }
469 if parsed.scheme == super::record_store::RECORDS_SCHEME {
470 if !pkg.get("records").and_then(Value::as_object).is_some_and(|r| r.contains_key(&parsed.name)) {
471 issues.push(issue(&here, format!("records:{} names no declared kind", parsed.name)));
472 }
473 if let Some(query) = parsed.params.get("query") {
474 if !pkg.get("queries").and_then(Value::as_object).is_some_and(|q| q.contains_key(query)) {
475 issues.push(issue(&here, format!("query {query} names no package query")));
476 }
477 }
478 }
479}
480
481fn issue_text(issues: &[PackageIssue]) -> String {
486 issues.iter().map(|i| format!("{} {}", if i.path.is_empty() { "/" } else { &i.path }, i.message)).collect::<Vec<_>>().join("; ")
487}
488
489pub fn run_action(pkg: &Value, name: &str, params: &Value, ctx: &ActionContext) -> Result<Value, String> {
493 let action = pkg.get("actions").and_then(Value::as_object).and_then(|a| a.get(name)).and_then(Value::as_object);
494 let Some((action, steps)) = action.and_then(|a| a.get("steps").and_then(Value::as_array).map(|s| (a, s))) else { return Err(format!("no action {name}")) };
495 let p = if params.is_null() { Value::Object(Map::new()) } else { params.clone() };
496 if !p.is_object() {
497 return Err("params is an object".into());
498 }
499 if let Some(schema) = action.get("params").filter(|s| s.is_object()) {
500 let issues = validate_record(schema, &p);
501 if !issues.is_empty() {
502 return Err(format!("params: {}", issue_text(&issues)));
503 }
504 }
505 let mut vars = Map::new();
506 vars.insert("params".into(), p);
507 vars.insert("data".into(), ctx.data.cloned().unwrap_or(Value::Null));
508 vars.insert("me".into(), ctx.me.map_or(Value::Null, |m| Value::String(m.into())));
509 let empty = Map::new();
510 let records = pkg.get("records").and_then(Value::as_object).unwrap_or(&empty);
511 let mut budget: i64 = ctx.max_steps.unwrap_or(ACTION_LIMIT_MAX_STEPS) as i64;
512 let mut writes = Vec::new();
513 let mut sets = Vec::new();
514 let mut calls = Vec::new();
515 for (i, step) in steps.iter().enumerate() {
516 let r#where = format!("step {i}");
517 let limit = || format!("{where}: action step limit exceeded");
518 budget -= 1;
519 if budget < 0 {
520 return Err(limit());
521 }
522 let mut value = |v: &Value| -> Result<Value, String> {
523 let Value::String(source) = v else { return Ok(v.clone()) };
524 let ast = parse_expression(source).map_err(|e| format!("{where}: {e}"))?;
525 let cap = (MAX_STEPS as i64).min(budget);
526 let env = ExpressionEnv { now: ctx.now, offset: ctx.offset, max_steps: Some(cap.max(0) as usize) };
527 let (result, used) = evaluate_counted(&ast, &vars, &env);
528 match result {
529 Err(e) if e == "step limit exceeded" && cap == budget => Err(limit()),
530 Err(e) => Err(format!("{where}: {e}")),
531 Ok(v) => {
532 budget -= used as i64;
533 if budget < 0 {
534 return Err(limit());
535 }
536 Ok(v)
537 }
538 }
539 };
540 let (Some(kind), Some(step)) = (step_kind(step), step.as_object()) else {
541 return Err(format!("{where}: a step is one of {}", ACTION_STEP_KINDS.join(", ")));
542 };
543 let body = &step[kind];
544 match kind {
545 "require" => {
546 let ok = value(body)?;
547 let Value::Bool(ok) = ok else { return Err(format!("{where}: require needs a bool, got {}", type_name(&ok))) };
548 if !ok {
549 return Err(match step.get("message").and_then(Value::as_str).filter(|m| !m.is_empty()) {
550 Some(m) => m.to_string(),
551 None => format!("{where}: requirement failed"),
552 });
553 }
554 }
555 "records.upsert" => {
556 let Some((body, data_exprs)) = body.as_object().and_then(|b| b.get("data").and_then(Value::as_object).map(|d| (b, d))) else {
557 return Err(format!("{where}: records.upsert is {{kind, data, id?}}"));
558 };
559 let kind_name = body.get("kind").and_then(Value::as_str).unwrap_or("");
560 let Some(schema) = records.get(kind_name).and_then(Value::as_object).and_then(|d| d.get("schema")).filter(|s| s.is_object()) else {
561 return Err(format!("{where}: records: no kind {kind_name}"));
562 };
563 let mut id = None;
564 if let Some(expr) = body.get("id") {
565 let v = value(expr)?;
566 match v {
567 Value::String(s) if !s.is_empty() => id = Some(s),
568 other => return Err(format!("{where}: id is a string, got {}", type_name(&other))),
569 }
570 }
571 let mut data = Map::new();
572 for field in sorted_keys(data_exprs) {
573 data.insert(field.clone(), value(&data_exprs[field.as_str()])?);
574 }
575 let data = Value::Object(data);
576 let issues = if id.is_none() {
578 validate_record(schema, &data)
579 } else {
580 let mut patch = schema.clone();
581 patch["required"] = json!([]);
582 validate_record(&patch, &data)
583 };
584 if !issues.is_empty() {
585 return Err(format!("{where}: records: {}", issue_text(&issues)));
586 }
587 writes.push(match id {
588 None => json!({"op": "upsert", "kind": kind_name, "data": data}),
589 Some(id) => json!({"op": "upsert", "kind": kind_name, "data": data, "id": id}),
590 });
591 }
592 "records.delete" => {
593 let Some(body) = body.as_object() else { return Err(format!("{where}: records.delete is {{id}}")) };
594 let v = match body.get("id") {
596 Some(expr) => value(expr)?,
597 None => return Err(format!("{where}: id is a string, got null")),
598 };
599 match v {
600 Value::String(s) if !s.is_empty() => writes.push(json!({"op": "delete", "id": s})),
601 other => return Err(format!("{where}: id is a string, got {}", type_name(&other))),
602 }
603 }
604 "set" => {
605 let Some((body, path)) = body.as_object().and_then(|b| b.get("path").and_then(Value::as_str).filter(|p| is_pointer(p)).map(|p| (b, p))) else {
606 return Err(format!("{where}: path is a JSON pointer"));
607 };
608 let v = value(body.get("value").unwrap_or(&Value::Null))?;
609 sets.push(json!({"path": path, "value": v}));
610 }
611 "call" => {
612 let Some(name) = body.as_str().filter(|n| function_effect(n) == FunctionEffect::Host && action_call_name(n).is_none()) else {
613 return Err(format!("{where}: call names a host function"));
614 };
615 let mut args = Map::new();
616 if let Some(given) = step.get("args").and_then(Value::as_object) {
617 for k in sorted_keys(given) {
618 args.insert(k.clone(), value(&given[k.as_str()])?);
619 }
620 }
621 calls.push(json!({"name": name, "args": args}));
622 }
623 _ => {}
624 }
625 }
626 Ok(json!({"writes": writes, "sets": sets, "calls": calls}))
627}
628
629pub fn apply_record_batch(rows: &[Value], writes: &[Value], records: Option<&Value>, me: Option<&str>, now: &str, new_id: impl FnMut(usize) -> String) -> Result<(Vec<Value>, Vec<String>), String> {
640 apply_record_batch_as(rows, writes, records, me, None, None, now, new_id)
641}
642
643#[allow(clippy::too_many_arguments)]
647pub fn apply_record_batch_as(
648 rows: &[Value],
649 writes: &[Value],
650 records: Option<&Value>,
651 me: Option<&str>,
652 role: Option<&str>,
653 access: Option<&str>,
654 now: &str,
655 mut new_id: impl FnMut(usize) -> String,
656) -> Result<(Vec<Value>, Vec<String>), String> {
657 use super::record_policy::{record_policy, record_refusal, RecordOp, RecordViewer};
658 let judged = role.is_some();
659 let viewer = RecordViewer { id: me, role, access };
660 let kind_def = |kind: &str| records.and_then(|r| r.get(kind));
661 let judge = |kind: &str, op: RecordOp, row: Option<&Value>| if judged { record_refusal(&record_policy(kind_def(kind)), op, &viewer, row) } else { None };
662 let readable = |r: &Value| !judged || record_refusal(&record_policy(kind_def(r.get("kind").and_then(Value::as_str).unwrap_or(""))), RecordOp::Read, &viewer, Some(r)).is_none();
664 let mut out: Vec<Value> = rows.to_vec();
665 let mut ids = Vec::new();
666 let mut creates = 0;
667 let row_id = |r: &Value| r.get("id").and_then(Value::as_str).map(str::to_string);
668 for (i, w) in writes.iter().enumerate() {
669 let r#where = format!("write {i}");
670 let wid = w.get("id").map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string));
671 if w.get("op").and_then(Value::as_str) == Some("delete") {
672 let id = wid.unwrap_or_else(|| "undefined".into());
673 let Some(row) = out.iter().find(|r| row_id(r).as_deref() == Some(id.as_str()) && readable(r)) else {
674 return Err(format!("{where}: record {id} not found"));
675 };
676 if let Some(refusal) = judge(row.get("kind").and_then(Value::as_str).unwrap_or(""), RecordOp::Delete, Some(row)) {
677 return Err(format!("{where}: {refusal}"));
678 }
679 out.retain(|r| row_id(r).as_deref() != Some(id.as_str()));
680 ids.push(id);
681 continue;
682 }
683 let kind = w.get("kind").and_then(Value::as_str).unwrap_or("undefined");
684 let Some(schema) = records.and_then(Value::as_object).and_then(|r| r.get(kind)).and_then(Value::as_object).and_then(|d| d.get("schema")).filter(|s| s.is_object()) else {
685 return Err(format!("{where}: records: no kind {kind}"));
686 };
687 let wdata = w.get("data").cloned().unwrap_or(Value::Null);
688 if let Some(id) = wid {
689 let Some(index) = out.iter().position(|r| row_id(r).as_deref() == Some(id.as_str()) && readable(r)) else { return Err(format!("{where}: record {id} not found")) };
690 if out[index].get("kind").and_then(Value::as_str) != Some(kind) {
691 return Err(format!("{where}: record {id} is not of kind {kind}"));
692 }
693 if let Some(refusal) = judge(kind, RecordOp::Update, Some(&out[index])) {
694 return Err(format!("{where}: {refusal}"));
695 }
696 let mut data = out[index].get("data").and_then(Value::as_object).cloned().unwrap_or_default();
697 if let Some(patch) = wdata.as_object() {
698 for (k, v) in patch {
699 data.insert(k.clone(), v.clone());
700 }
701 }
702 let data = Value::Object(data);
703 let issues = validate_record(schema, &data);
704 if !issues.is_empty() {
705 return Err(format!("{where}: records: {}", issue_text(&issues)));
706 }
707 if let Some(row) = out[index].as_object_mut() {
708 row.insert("data".into(), data);
709 row.insert("updatedAt".into(), Value::String(now.into()));
710 }
711 ids.push(id);
712 continue;
713 }
714 if let Some(refusal) = judge(kind, RecordOp::Create, None) {
715 return Err(format!("{where}: {refusal}"));
716 }
717 let issues = validate_record(schema, &wdata);
718 if !issues.is_empty() {
719 return Err(format!("{where}: records: {}", issue_text(&issues)));
720 }
721 let id = new_id(creates);
722 creates += 1;
723 out.push(json!({"id": id, "kind": kind, "data": wdata, "createdBy": me, "authorId": me, "createdAt": now, "updatedAt": now}));
724 ids.push(id);
725 }
726 Ok((out, ids))
727}