feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
Antes: parse_field_value(EntityRef, raw) devolvía Ok(json!(raw)) blindly. Garbage entraba al log/store si el field se usaba como seed o param (sólo el path de morphism inputs validaba UUID). Ahora: Uuid::parse_str(raw.trim()) → error claro si falla, value trimmed si pasa. El selector clickable garantiza happy path; este check es defensivo contra paste manual o garbage tipeado. 5 tests nuevos: happy path, trim de whitespace, rechazo de garbage, rechazo de empty, propagación de error a resolve_param_value con label del FieldSpec en el mensaje. 35 tests verdes. E2E del morphism real intacto (sus inputs van por path dedicado, no por parse_field_value). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,52 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-09
|
## 2026-05-09
|
||||||
|
|
||||||
|
### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
|
||||||
|
Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
|
||||||
|
devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
|
||||||
|
incluso si era basura. La validación de UUID sólo ocurría cuando el
|
||||||
|
field se usaba como **input** del morphism (línea ~540 de
|
||||||
|
`commit_morphism`); como **seed field** o como **param**, garbage
|
||||||
|
pasaba silenciosamente.
|
||||||
|
|
||||||
|
Cambios:
|
||||||
|
- **`parse_field_value(EntityRef, raw)`** ahora hace
|
||||||
|
`Uuid::parse_str(raw.trim())` y devuelve error claro si falla:
|
||||||
|
`"'<raw>' no es UUID válido (usá el selector de records)"`. En
|
||||||
|
caso de éxito, devuelve el UUID **trimmed** como string —
|
||||||
|
protege contra paste manual con whitespace.
|
||||||
|
- **Doble cobertura**: este path cubre seed fields (commit_seed via
|
||||||
|
obj.insert) y morphism params (resolve_param_value lo invoca por
|
||||||
|
cada FieldSpec con kind=EntityRef). El path de morphism inputs
|
||||||
|
ya validaba antes con `Uuid::parse_str` directo — sigue intacto,
|
||||||
|
no hay double-validation.
|
||||||
|
- **Selector clickable es happy path**: el dropdown setea valores
|
||||||
|
bien-formados, así que el usuario nunca debería ver el error en
|
||||||
|
uso normal. Sólo dispara con paste manual o si el usuario escribe
|
||||||
|
garbage en el input — defensivo.
|
||||||
|
|
||||||
|
5 tests nuevos (reemplazan al obsoleto `parse_field_entity_ref_returns_string`):
|
||||||
|
- `parse_field_entity_ref_accepts_valid_uuid` — happy path.
|
||||||
|
- `parse_field_entity_ref_trims_whitespace` — `" uuid\n"` → `"uuid"`.
|
||||||
|
- `parse_field_entity_ref_rejects_non_uuid` — `"abc-123"` → error
|
||||||
|
con el value y la palabra "UUID" en el mensaje.
|
||||||
|
- `parse_field_entity_ref_rejects_empty_string` — `""` → rebota.
|
||||||
|
- `resolve_param_strict_entity_ref_propagates_error` — sanity de
|
||||||
|
que el wireup en resolve_param_value hereda el strict checking,
|
||||||
|
con label del FieldSpec en el mensaje.
|
||||||
|
|
||||||
|
35 tests verdes (+4 net). El E2E del morphism real
|
||||||
|
`morphism_pipeline_executes_real_sales_vender` sigue verde — sus
|
||||||
|
inputs van por el path dedicado, no por parse_field_value.
|
||||||
|
|
||||||
|
Pendientes restantes:
|
||||||
|
- **Atajo Esc para Cancelar** del modal de delete.
|
||||||
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
|
||||||
|
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
||||||
|
- **Validación cross-field** (ej: el UUID del EntityRef existe en
|
||||||
|
la entity referida) — hoy sólo validamos forma; un UUID válido
|
||||||
|
pero inexistente sí pasa.
|
||||||
|
|
||||||
### feat(nakui-ui): snapshot/compaction automático del event log al startup
|
### feat(nakui-ui): snapshot/compaction automático del event log al startup
|
||||||
Cierra el último gran pendiente del round: el replay full cada
|
Cierra el último gran pendiente del round: el replay full cada
|
||||||
startup escala lineal en el log. Con 60+ entries el costo de boot
|
startup escala lineal en el log. Con 60+ entries el costo de boot
|
||||||
|
|||||||
@@ -865,9 +865,20 @@ fn parse_field_value(kind: FieldKind, raw: &str) -> Result<Value, String> {
|
|||||||
match kind {
|
match kind {
|
||||||
FieldKind::Text | FieldKind::Multiline | FieldKind::Date => Ok(json!(raw)),
|
FieldKind::Text | FieldKind::Multiline | FieldKind::Date => Ok(json!(raw)),
|
||||||
// EntityRef se almacena como string del UUID seleccionado.
|
// EntityRef se almacena como string del UUID seleccionado.
|
||||||
// El commit_morphism luego lo parsea como Uuid para inputs;
|
// Validamos que parsee como UUID al submit — antes esto se
|
||||||
// en seed_entity normal queda como string en el record.
|
// chequeaba sólo para morphism inputs (línea ~540), pero un
|
||||||
FieldKind::EntityRef => Ok(json!(raw)),
|
// EntityRef como SEED field o como param de morphism caía
|
||||||
|
// de la heurística silenciosa. Ahora rebota con mensaje
|
||||||
|
// claro acá, antes de tocar el log o el morphism Rhai.
|
||||||
|
// El selector clickable garantiza UUIDs válidos en happy
|
||||||
|
// path; este check protege paste manual o garbage.
|
||||||
|
FieldKind::EntityRef => {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
Uuid::parse_str(trimmed).map_err(|_| {
|
||||||
|
format!("'{raw}' no es UUID válido (usá el selector de records)")
|
||||||
|
})?;
|
||||||
|
Ok(json!(trimmed))
|
||||||
|
}
|
||||||
FieldKind::Boolean => match raw.to_ascii_lowercase().as_str() {
|
FieldKind::Boolean => match raw.to_ascii_lowercase().as_str() {
|
||||||
"true" | "yes" | "1" | "on" | "y" => Ok(json!(true)),
|
"true" | "yes" | "1" | "on" | "y" => Ok(json!(true)),
|
||||||
"" | "false" | "no" | "0" | "off" | "n" => Ok(json!(false)),
|
"" | "false" | "no" | "0" | "off" | "n" => Ok(json!(false)),
|
||||||
@@ -2024,12 +2035,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_field_entity_ref_returns_string() {
|
fn parse_field_entity_ref_accepts_valid_uuid() {
|
||||||
// EntityRef se almacena como string del UUID. parse_field_value
|
let id = Uuid::new_v4();
|
||||||
// no lo valida como UUID — eso lo hace commit_morphism al
|
let v = parse_field_value(FieldKind::EntityRef, &id.to_string()).unwrap();
|
||||||
// resolver inputs.
|
assert_eq!(v, json!(id.to_string()));
|
||||||
let v = parse_field_value(FieldKind::EntityRef, "abc-123").unwrap();
|
}
|
||||||
assert_eq!(v, json!("abc-123"));
|
|
||||||
|
#[test]
|
||||||
|
fn parse_field_entity_ref_trims_whitespace() {
|
||||||
|
// El selector clickable garantiza el value pelado; este check
|
||||||
|
// protege contra paste manual con espacios accidentales.
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let padded = format!(" {id}\n");
|
||||||
|
let v = parse_field_value(FieldKind::EntityRef, &padded).unwrap();
|
||||||
|
assert_eq!(v, json!(id.to_string()), "debería trimear y devolver el UUID limpio");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_field_entity_ref_rejects_non_uuid() {
|
||||||
|
let err = parse_field_value(FieldKind::EntityRef, "abc-123").unwrap_err();
|
||||||
|
assert!(err.contains("'abc-123'"), "msg debe mencionar el value: {err}");
|
||||||
|
assert!(
|
||||||
|
err.contains("UUID") || err.contains("uuid"),
|
||||||
|
"msg debe mencionar UUID: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_field_entity_ref_rejects_empty_string() {
|
||||||
|
// Un EntityRef vacío al submit: el form lo manda como ""
|
||||||
|
// si el usuario no clickeó nada. Debería rebotar acá en
|
||||||
|
// lugar de loguear "" como un record id basura.
|
||||||
|
let err = parse_field_value(FieldKind::EntityRef, "").unwrap_err();
|
||||||
|
assert!(err.contains("UUID"), "msg debe mencionar UUID: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_param_strict_entity_ref_propagates_error() {
|
||||||
|
// Sanity: resolve_param_value con kind=EntityRef invoca
|
||||||
|
// parse_field_value y propaga el error de UUID inválido,
|
||||||
|
// con el label del FieldSpec en el mensaje.
|
||||||
|
let s = spec("stock_ref", FieldKind::EntityRef, true);
|
||||||
|
let err = resolve_param_value("stock_ref", "not-a-uuid", Some(&s)).unwrap_err();
|
||||||
|
assert!(err.contains("stock_ref"), "msg debe incluir label: {err}");
|
||||||
|
assert!(err.contains("UUID"), "msg debe mencionar UUID: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user