Skip to content

The read-only strip still logs a WARN calling the addressed row's own id a forged caller write, on every single-record update of a platform object #8141

Description

@os-zhuang

Split out of #8093 rather than fixed there, because the fix is a judgement call about a shared strip helper and #8093's scope fence is the report channel.

What #8093 fixed, and what it left

#8093 stopped droppedFields / onFieldsDropped naming the row's own primary key on a by-id update: the payload id that equals the bound row is the write's address, not payload, so it is not a drop. That closed the user-facing amber toast.

The log channel still says the untrue thing. stripReadonlyFields (packages/objectql/src/validation/rule-validator.ts) is unchanged by that PR — deliberately, since the strip itself must keep removing id from the SET clause — and it emits its readonlyStripWarning whenever it deletes a key.

Measured, after #8093's fix

A real ObjectQL + a real ObjectStackProtocolImplementation, updateData({ object: 'sys_user_preference', id: 'rec_1', data: { value: ['x'] } }) — a body with no id key:

droppedFields : null            ← #8093's fix, correct
warn count    : 1
  WARN: Field 'id' on 'sys_user_preference' is read-only: the caller-supplied
        value was DROPPED and the update is being COMMITTED WITHOUT IT — ...

Every one of those three claims is false for this write. The value was not caller-supplied (metadata-protocol's updateData folded the path id in, #6479); nothing the caller wanted was dropped; and the update was not "committed without" anything it asked for.

Why it is worth a card and not a shrug

  1. Volume. It fires on every single-record PATCH of every object declaring id as readonly: true — which is every platform object. The console's recents trace alone emits one per org switch. This is the log-side version of the exact "drown the real signal" cost sys_user_preference update reports droppedFields:[id] for a key the caller never sent — every org switch pops a user-facing warning toast #8093, rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 and 审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794 were about; readonlyStripWarning's own doc argues it stays at warn precisely so real forgery attempts are visible, and this buries them.
  2. The remedy prose is wrong here. The message tells the reader to pass { context: { isSystem: true } }. Following that advice for this write would exempt the caller from the static read-only strip entirely — a strictly worse posture, adopted to silence a line that should never have printed.

What the fix is NOT

⛔ Not "stop stripping id". The strip must stay: a same-value primary-key write is a harmless no-op on SQL but an outright rejection on stores with immutable primary keys, and #6435's block already ruled that widening the strip to the truthy-scalar data.id case "is a separate decision, not a rider here".

Options for whoever takes it

  • A. Teach stripReadonlyFields which key is the write's address (an option alongside preserveAudit) and skip only the log line for it. Contained, but adds a parameter to a helper shared by the by-id branch, the multi branch and the insert-side sibling.
  • B. Have the by-id branch remove the addressing id before the read-only pass, as addressing rather than as a read-only strip — no strip, no warn, no report, one concept in one place. Bigger blast radius: it changes what the driver receives for objects whose id is not readonly, which is update 的 **by-id** 路径同样把非标量 data.id 交给驱动写主键列(#6262 的孪生形状,where.id 胜出时) #6435's explicitly separate decision.
  • C. Leave it and accept the log noise.

Recommendation from the #8093 lane: A, with B filed as the follow-up if the "addressing is not payload" idea is worth unifying across both channels.

Pointers

Filed unassigned by the #8093 dev lane (seat #6019, domain:engine-core).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions