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 {
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 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 }
391 for (fk, fv) in fc {
392 if fk != "call" {
393 pure(fv, &format!("{hat}/functionCall/{}", escape(fk)), &mut issues);
394 }
395 }
396 }
397 }
398 }
399 }
400 }
401 issues
402}
403
404fn issue_text(issues: &[PackageIssue]) -> String {
409 issues.iter().map(|i| format!("{} {}", if i.path.is_empty() { "/" } else { &i.path }, i.message)).collect::<Vec<_>>().join("; ")
410}
411
412pub fn run_action(pkg: &Value, name: &str, params: &Value, ctx: &ActionContext) -> Result<Value, String> {
416 let action = pkg.get("actions").and_then(Value::as_object).and_then(|a| a.get(name)).and_then(Value::as_object);
417 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}")) };
418 let p = if params.is_null() { Value::Object(Map::new()) } else { params.clone() };
419 if !p.is_object() {
420 return Err("params is an object".into());
421 }
422 if let Some(schema) = action.get("params").filter(|s| s.is_object()) {
423 let issues = validate_record(schema, &p);
424 if !issues.is_empty() {
425 return Err(format!("params: {}", issue_text(&issues)));
426 }
427 }
428 let mut vars = Map::new();
429 vars.insert("params".into(), p);
430 vars.insert("data".into(), ctx.data.cloned().unwrap_or(Value::Null));
431 vars.insert("me".into(), ctx.me.map_or(Value::Null, |m| Value::String(m.into())));
432 let empty = Map::new();
433 let records = pkg.get("records").and_then(Value::as_object).unwrap_or(&empty);
434 let mut budget: i64 = ctx.max_steps.unwrap_or(ACTION_LIMIT_MAX_STEPS) as i64;
435 let mut writes = Vec::new();
436 let mut sets = Vec::new();
437 let mut calls = Vec::new();
438 for (i, step) in steps.iter().enumerate() {
439 let r#where = format!("step {i}");
440 let limit = || format!("{where}: action step limit exceeded");
441 budget -= 1;
442 if budget < 0 {
443 return Err(limit());
444 }
445 let mut value = |v: &Value| -> Result<Value, String> {
446 let Value::String(source) = v else { return Ok(v.clone()) };
447 let ast = parse_expression(source).map_err(|e| format!("{where}: {e}"))?;
448 let cap = (MAX_STEPS as i64).min(budget);
449 let env = ExpressionEnv { now: ctx.now, offset: ctx.offset, max_steps: Some(cap.max(0) as usize) };
450 let (result, used) = evaluate_counted(&ast, &vars, &env);
451 match result {
452 Err(e) if e == "step limit exceeded" && cap == budget => Err(limit()),
453 Err(e) => Err(format!("{where}: {e}")),
454 Ok(v) => {
455 budget -= used as i64;
456 if budget < 0 {
457 return Err(limit());
458 }
459 Ok(v)
460 }
461 }
462 };
463 let (Some(kind), Some(step)) = (step_kind(step), step.as_object()) else {
464 return Err(format!("{where}: a step is one of {}", ACTION_STEP_KINDS.join(", ")));
465 };
466 let body = &step[kind];
467 match kind {
468 "require" => {
469 let ok = value(body)?;
470 let Value::Bool(ok) = ok else { return Err(format!("{where}: require needs a bool, got {}", type_name(&ok))) };
471 if !ok {
472 return Err(match step.get("message").and_then(Value::as_str).filter(|m| !m.is_empty()) {
473 Some(m) => m.to_string(),
474 None => format!("{where}: requirement failed"),
475 });
476 }
477 }
478 "records.upsert" => {
479 let Some((body, data_exprs)) = body.as_object().and_then(|b| b.get("data").and_then(Value::as_object).map(|d| (b, d))) else {
480 return Err(format!("{where}: records.upsert is {{kind, data, id?}}"));
481 };
482 let kind_name = body.get("kind").and_then(Value::as_str).unwrap_or("");
483 let Some(schema) = records.get(kind_name).and_then(Value::as_object).and_then(|d| d.get("schema")).filter(|s| s.is_object()) else {
484 return Err(format!("{where}: records: no kind {kind_name}"));
485 };
486 let mut id = None;
487 if let Some(expr) = body.get("id") {
488 let v = value(expr)?;
489 match v {
490 Value::String(s) if !s.is_empty() => id = Some(s),
491 other => return Err(format!("{where}: id is a string, got {}", type_name(&other))),
492 }
493 }
494 let mut data = Map::new();
495 for field in sorted_keys(data_exprs) {
496 data.insert(field.clone(), value(&data_exprs[field.as_str()])?);
497 }
498 let data = Value::Object(data);
499 let issues = if id.is_none() {
501 validate_record(schema, &data)
502 } else {
503 let mut patch = schema.clone();
504 patch["required"] = json!([]);
505 validate_record(&patch, &data)
506 };
507 if !issues.is_empty() {
508 return Err(format!("{where}: records: {}", issue_text(&issues)));
509 }
510 writes.push(match id {
511 None => json!({"op": "upsert", "kind": kind_name, "data": data}),
512 Some(id) => json!({"op": "upsert", "kind": kind_name, "data": data, "id": id}),
513 });
514 }
515 "records.delete" => {
516 let Some(body) = body.as_object() else { return Err(format!("{where}: records.delete is {{id}}")) };
517 let v = match body.get("id") {
519 Some(expr) => value(expr)?,
520 None => return Err(format!("{where}: id is a string, got null")),
521 };
522 match v {
523 Value::String(s) if !s.is_empty() => writes.push(json!({"op": "delete", "id": s})),
524 other => return Err(format!("{where}: id is a string, got {}", type_name(&other))),
525 }
526 }
527 "set" => {
528 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 {
529 return Err(format!("{where}: path is a JSON pointer"));
530 };
531 let v = value(body.get("value").unwrap_or(&Value::Null))?;
532 sets.push(json!({"path": path, "value": v}));
533 }
534 "call" => {
535 let Some(name) = body.as_str().filter(|n| function_effect(n) == FunctionEffect::Host && action_call_name(n).is_none()) else {
536 return Err(format!("{where}: call names a host function"));
537 };
538 let mut args = Map::new();
539 if let Some(given) = step.get("args").and_then(Value::as_object) {
540 for k in sorted_keys(given) {
541 args.insert(k.clone(), value(&given[k.as_str()])?);
542 }
543 }
544 calls.push(json!({"name": name, "args": args}));
545 }
546 _ => {}
547 }
548 }
549 Ok(json!({"writes": writes, "sets": sets, "calls": calls}))
550}
551
552pub 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> {
563 apply_record_batch_as(rows, writes, records, me, None, None, now, new_id)
564}
565
566#[allow(clippy::too_many_arguments)]
570pub fn apply_record_batch_as(
571 rows: &[Value],
572 writes: &[Value],
573 records: Option<&Value>,
574 me: Option<&str>,
575 role: Option<&str>,
576 access: Option<&str>,
577 now: &str,
578 mut new_id: impl FnMut(usize) -> String,
579) -> Result<(Vec<Value>, Vec<String>), String> {
580 use super::record_policy::{record_policy, record_refusal, RecordOp, RecordViewer};
581 let judged = role.is_some();
582 let viewer = RecordViewer { id: me, role, access };
583 let kind_def = |kind: &str| records.and_then(|r| r.get(kind));
584 let judge = |kind: &str, op: RecordOp, row: Option<&Value>| if judged { record_refusal(&record_policy(kind_def(kind)), op, &viewer, row) } else { None };
585 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();
587 let mut out: Vec<Value> = rows.to_vec();
588 let mut ids = Vec::new();
589 let mut creates = 0;
590 let row_id = |r: &Value| r.get("id").and_then(Value::as_str).map(str::to_string);
591 for (i, w) in writes.iter().enumerate() {
592 let r#where = format!("write {i}");
593 let wid = w.get("id").map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string));
594 if w.get("op").and_then(Value::as_str) == Some("delete") {
595 let id = wid.unwrap_or_else(|| "undefined".into());
596 let Some(row) = out.iter().find(|r| row_id(r).as_deref() == Some(id.as_str()) && readable(r)) else {
597 return Err(format!("{where}: record {id} not found"));
598 };
599 if let Some(refusal) = judge(row.get("kind").and_then(Value::as_str).unwrap_or(""), RecordOp::Delete, Some(row)) {
600 return Err(format!("{where}: {refusal}"));
601 }
602 out.retain(|r| row_id(r).as_deref() != Some(id.as_str()));
603 ids.push(id);
604 continue;
605 }
606 let kind = w.get("kind").and_then(Value::as_str).unwrap_or("undefined");
607 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 {
608 return Err(format!("{where}: records: no kind {kind}"));
609 };
610 let wdata = w.get("data").cloned().unwrap_or(Value::Null);
611 if let Some(id) = wid {
612 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")) };
613 if out[index].get("kind").and_then(Value::as_str) != Some(kind) {
614 return Err(format!("{where}: record {id} is not of kind {kind}"));
615 }
616 if let Some(refusal) = judge(kind, RecordOp::Update, Some(&out[index])) {
617 return Err(format!("{where}: {refusal}"));
618 }
619 let mut data = out[index].get("data").and_then(Value::as_object).cloned().unwrap_or_default();
620 if let Some(patch) = wdata.as_object() {
621 for (k, v) in patch {
622 data.insert(k.clone(), v.clone());
623 }
624 }
625 let data = Value::Object(data);
626 let issues = validate_record(schema, &data);
627 if !issues.is_empty() {
628 return Err(format!("{where}: records: {}", issue_text(&issues)));
629 }
630 if let Some(row) = out[index].as_object_mut() {
631 row.insert("data".into(), data);
632 row.insert("updatedAt".into(), Value::String(now.into()));
633 }
634 ids.push(id);
635 continue;
636 }
637 if let Some(refusal) = judge(kind, RecordOp::Create, None) {
638 return Err(format!("{where}: {refusal}"));
639 }
640 let issues = validate_record(schema, &wdata);
641 if !issues.is_empty() {
642 return Err(format!("{where}: records: {}", issue_text(&issues)));
643 }
644 let id = new_id(creates);
645 creates += 1;
646 out.push(json!({"id": id, "kind": kind, "data": wdata, "createdBy": me, "authorId": me, "createdAt": now, "updatedAt": now}));
647 ids.push(id);
648 }
649 Ok((out, ids))
650}