You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Update-side: a readonly field is stripped from persistence but still reaches beforeUpdate, so hook-derived columns persist values computed from data the row never contains #16344
Measured on a real app (ObjectStack 17.2.0, sqlite driver, dev runtime) while verifying an Excel import round trip.
现象
On update, a value sent for a field the object declares readonly: true is not persisted (read-back shows the old value — the strip works), but it is visible to the object's own beforeUpdate lifecycle hook. The hook computes derived fields from it, and those derived writes DO persist.
The stored row is then internally self-contradictory, silently: the readonly columns show their correct values, while the derived columns and the human-readable calculation trace were computed from values that were never stored and are nowhere on the record.
This is the update-side sibling of #15964 (which is about created_at surviving the create-side strip). Here the strip itself is fine — what leaks is the hook input.
target_value / weight are unchanged at 400 / 10 (strip works), yet the persisted score is 1.2 and the persisted trace names 目标 1 / 权重 1% — a record whose own audit trail cites values it does not contain.
Notes on scope:
Not import-specific — the plain REST PATCH above is the whole repro. The console's import wizard hits the same path and produces the same rows (a spreadsheet with the two readonly columns edited imports as 全部 4 行均有效 → 更新 4 条, leaving four contradictory rows).
No error, no warning, no droppedFields on the response — the write reports success.
The caller here is the built-in dev admin; no special privilege was used.
期望能力
The record the write path hands to lifecycle hooks should be the record it intends to persist. Concretely, one of:
strip readonly fields from the payload before hooks see it, so a hook can never compute from a value that will not be stored; or
Option 1 is the one that makes the invariant hold without every application re-implementing the check: today the only way for an app to be safe is to re-read its own readonly fields inside every hook and ignore the incoming record — which defeats the point of declaring them readonly.
Measured on a real app (ObjectStack 17.2.0, sqlite driver, dev runtime) while verifying an Excel import round trip.
现象
On update, a value sent for a field the object declares
readonly: trueis not persisted (read-back shows the old value — the strip works), but it is visible to the object's ownbeforeUpdatelifecycle hook. The hook computes derived fields from it, and those derived writes DO persist.The stored row is then internally self-contradictory, silently: the readonly columns show their correct values, while the derived columns and the human-readable calculation trace were computed from values that were never stored and are nowhere on the record.
This is the update-side sibling of #15964 (which is about
created_atsurviving the create-side strip). Here the strip itself is fine — what leaks is the hook input.最小复现
Object
kpi_entry_linedeclares:and a
beforeUpdatehook that recomputescompletion_rate/score_rate/score/calc_tracefromactual_value,target_valueandweight.Baseline — write only the writable field:
Same request plus values for the two
readonlyfields:target_value/weightare unchanged at 400 / 10 (strip works), yet the persistedscoreis 1.2 and the persisted trace names目标 1/权重 1%— a record whose own audit trail cites values it does not contain.Notes on scope:
全部 4 行均有效→更新 4 条, leaving four contradictory rows).droppedFieldson the response — the write reports success.期望能力
The record the write path hands to lifecycle hooks should be the record it intends to persist. Concretely, one of:
readonlyfields from the payload before hooks see it, so a hook can never compute from a value that will not be stored; orreadonlyfield is present (theReadonlyFieldRejectedErrorfamily in objectql: 11 error classes still spell their code as an inline literal, so a consumer cannot follow theby code, not instanceofconvention the docs already teach #16159 / finding: the docs teachcatch it by codefor ReadonlyFieldRejectedError but still do not name the constant #16159 now publishes #16281 looks like the natural home); ordroppedFieldscontract finding: cloneData is the one create face whose response contract carries no droppedFields — a clone that copies or overrides a readonly column is stripped and warned, but not reported on the wire #15703 asks for oncloneData) so an application can detect and refuse.Option 1 is the one that makes the invariant hold without every application re-implementing the check: today the only way for an app to be safe is to re-read its own readonly fields inside every hook and ignore the incoming record — which defeats the point of declaring them readonly.
平台版本
@objectstack/*17.2.0