feat(nakui-ui): validación cross-field del EntityRef (existence en store)
parse_field_value(EntityRef) sólo validaba forma (UUID parseable). Un UUID válido pero inexistente pasaba al log/store, dejando dangling references. Ahora validamos también existencia contra la entity declarada en FieldSpec.ref_entity. - Nuevo helper validate_entity_refs<S: Store>(store, refs): fail-fast loop sobre (label, target_entity, uuid) tuples; primer record ausente → error con label legible + UUID corto. - commit_seed: durante el parse loop encolamos cada EntityRef + ref_entity + UUID parseado. Después del loop, una sola toma del store lock valida todos. Falla early: ningún log entry. - Cobertura: SEED + EDIT. Morphism inputs ya cubierto por Executor::compute (load + EntityMissing) — documentado en el doc del helper. 5 tests nuevos del helper: happy path, fail-fast con detalles, label en msg, lista vacía, distingue target entity. 45 tests verdes en nakui-ui. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,71 @@ ratio/diff ver `git show <sha>`.
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
|
||||
Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
|
||||
sólo validaba **forma** (UUID parseable + trim de whitespace) — un
|
||||
UUID válido pero inexistente en el store pasaba silenciosamente al
|
||||
log/store, dejando dangling references. Ahora validamos también
|
||||
**existencia** contra la entity declarada en `FieldSpec.ref_entity`.
|
||||
|
||||
Cambios:
|
||||
- **Nuevo helper `validate_entity_refs<S: Store>(store, refs)`**:
|
||||
- `refs: &[(label, target_entity, uuid)]`.
|
||||
- Loop fail-fast: primer record ausente → error con label
|
||||
legible + UUID corto + target entity en el msg
|
||||
`"campo 'Stock': record abc12345 de 'Stock' no existe en el store"`.
|
||||
- Pure (toma `&S: Store`), totalmente testable sin GPUI.
|
||||
- **Wireup en `commit_seed`**:
|
||||
- Durante el parse loop, cuando un field es EntityRef + tiene
|
||||
`ref_entity` declarado + value parseado a UUID, lo encolamos
|
||||
en `entity_refs: Vec<(String, String, Uuid)>`.
|
||||
- Después del parse loop (antes del seed/edit branch), si
|
||||
`entity_refs` no está vacío, una sola toma del store lock
|
||||
para validar todos via el helper.
|
||||
- Falla early: ningún log entry, ningún apply.
|
||||
- **Cobertura**:
|
||||
- SEED path: alta nueva con EntityRef → validamos antes de
|
||||
`Seed { data }`.
|
||||
- EDIT path: edit con EntityRef → validamos antes de calcular
|
||||
delta. Una optional empty (que iría a clear) no cuenta como
|
||||
EntityRef (raw vacío skipea el push).
|
||||
- Morphism inputs: NO se duplica acá. `Executor::compute` ya
|
||||
valida cada input via `store.load(...).ok_or(EntityMissing)`
|
||||
antes de correr el script Rhai. Documentado en el doc del
|
||||
helper.
|
||||
|
||||
5 tests nuevos:
|
||||
- `validate_entity_refs_passes_when_all_records_exist` — happy path.
|
||||
- `validate_entity_refs_fails_on_first_missing` — fail-fast con
|
||||
msg que incluye entity + UUID corto.
|
||||
- `validate_entity_refs_uses_label_not_entity_in_msg` — el label
|
||||
legible (ej: "Stock origen") aparece en el error, no la entity
|
||||
desnuda.
|
||||
- `validate_entity_refs_empty_list_is_ok` — lista vacía es Ok.
|
||||
- `validate_entity_refs_distinguishes_target_from_other_entities` —
|
||||
un UUID que existe bajo Customer pero NO Stock falla la
|
||||
validación contra Stock.
|
||||
|
||||
45 tests verdes en nakui-ui (+5). Workspace build verde.
|
||||
|
||||
Comportamiento esperado:
|
||||
- **Selector clickable es happy path**: el dropdown sólo lista
|
||||
records existentes, así que clickearlo nunca debería disparar
|
||||
el error. Sólo dispara con paste manual de UUID que no existe
|
||||
o records borrados después de la selección (timing race).
|
||||
- **Optional empty no se valida**: si el field es EntityRef
|
||||
optional y el form lo deja vacío, lo manejamos como "no value"
|
||||
(skipea el push a `entity_refs`); la lógica de Clear se
|
||||
encarga del resto.
|
||||
- **Lock contention**: una sola toma del store lock por
|
||||
`commit_seed`, no una por field. La validación es O(refs) reads.
|
||||
|
||||
Pendientes restantes:
|
||||
- **Validación KCL del record post-edit** antes de emitir Set/Clear
|
||||
(hoy `ui.edit_record` no pasa por `Executor::compute`).
|
||||
- **EntityRef cross-module** (referenciar records de OTRO módulo
|
||||
por nombre, no sólo por entity local).
|
||||
|
||||
### feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
|
||||
Cierra el último pendiente de UX del round. El edit no podía
|
||||
"borrar" un value vaciando el input — empty optional fields
|
||||
|
||||
Reference in New Issue
Block a user