fix(metadata-protocol): PATCH /data/:object/:id binds the PATH row, not the body's id (#6479) - #6709
Merged
Merged
Conversation
…t the body's id (#6479) `updateData` probed existence and validated OCC against the path `:id`, built `{ where: { id: request.id } }`, and then handed the request body to the engine verbatim — where a truthy scalar `data.id` outranks `where.id`. A body of `{"id":"rec_2"}` on `PATCH /data/task/rec_1` therefore probed rec_1, version-checked rec_1, WROTE rec_2, and answered `id: rec_1` beside rec_2's readback: a silent cross-row write straight past the caller's own `If-Match`. The path id is now merged over the payload before dispatch (`{ ...request.data, id: request.id }`) — the same shape the bulk ingress in `rest-server.ts` has always used, so the repo's two single-write ingresses give one answer (#4550 / #4434). Triage ruling A of 2026-08-08; routes B (400 on mismatch) and C (schema ban) were explicitly rejected, and neither the engine's payload-first dispatch (#5748) nor its by-id payload strip (#6435) is touched. A non-record payload (`undefined`, `null`, an array) passes through untouched so this ingress is never kinder than the producer about a malformed call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 8, 2026
os-zhuang
marked this pull request as ready for review
August 8, 2026 13:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6479
结论先说
PATCH /data/:object/:id这一条 ingress 上,行标识以路径:id为准。请求体里的标量id不再能把写落到别的行上。实现落在
packages/metadata-protocol/src/protocol.ts的updateData区,一处派发前的载荷合并;引擎、packages/spec都没动。前提复核(在
origin/main上逐条读过,不是照抄 issue)Issue 说的三方打架,在合并后的 main 上完整成立(行号已漂移,以下按当时
e6025e9重新定位,现已再合并至d6d1a50be):protocol.ts:5743const current = await this.probeRecord(request.object, request.id)—— 存在性探测判路径 id;:5748this.assertVersionOf(request.object, request.id, current, request.expectedVersion)—— OCC 判路径 id;:5749const opts = { where: { id: request.id } },:5759await this.engine.update(request.object, request.data, opts)——request.data原样下发;:5760–5764回执{ id: request.id, record: result }。而
packages/metadata-core/src/engine-update-dispatch.ts的规则是载荷优先,案例表里写着a SCALAR data.id still wins over a scalar where.id(expectId: 'rec_1')。两者相加:PATCH /data/task/rec_1带 body{"id":"rec_2","title":"x"}会探测 rec_1、按 rec_1 校If-Match、写 rec_2、再回一个id: rec_1配 rec_2 的回读。rec_2 从未被探测、也从未被 OCC 校验。线上到引擎之间确实没有任何一层剥
id:rest-server.ts的 PATCH 路由只解构掉expectedVersion;UpdateDataRequestSchema把data声明为z.record(z.string(), z.unknown())。做法(裁决路线 A)
派发前把路径 id 盖到载荷上,和批量 ingress 对同一问题的既有答案同形:
于是探测的行、OCC 校的行、真正写的行、回执
id/record指的行,是同一行 —— URL 里那一行。一个仓库对「一次单行写绑定哪一行」只剩一个答案(#4550 / #4434 那一族)。几点边界,都是刻意的:
by-id,绑定的还是where本来就带着的那个 id。ObjectQL.update 的data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 的载荷优先规则、update的 **by-id** 路径同样把非标量data.id交给驱动写主键列(#6262 的孪生形状,where.id胜出时) #6435 / PR fix(objectql): by-id 更新不再把「已判定不是主键」的载荷id写进 SET 子句 (#6435) #6475 的 by-id 载荷剥离,都没碰,ENGINE_UPDATE_DISPATCH_CASES一行未动。UpdateDataRequestSchema层禁id」;前者在已发布 API 上新增一条拒绝,后者改的是被接受的请求形状。本 PR 两者都不做,接受的 body 形状一字未变。undefined/null/ 数组)。引擎是故意不做保护地读data.id的,所以undefined在那边就该是TypeError;ingress 若在这里合并出一个{ id },就会把一次坏调用答成一次成功的空写 —— 比 producer 更宽容,正是engine-update-dispatch.ts存在的目的所要防的那件事。这些形状本身也没有标量id去压where.id,所以不变式对它们靠opts.where就成立。import-runner.ts每匹配一行都走这条 ingress,body 里带id是 CSV 导入的常态,逐行 warn 会在导入路径上产生无界日志;批量 ingress 也一直是静默覆盖。两条 ingress 保持同一种沉默。测试
新增
packages/metadata-protocol/src/protocol.update-path-id-wins.test.ts(16 例)。钉的是 ingress 级不变式,不是「这行代码在」:探测的行、OCC 校的行、写的行、回执的id/record,是同一行 —— 路径行。fake engine 的
update不自己复刻规则,而是调用 producer 的assertEngineUpdateDispatch(data, options)并服从它给出的绑定行(delete同样开在assertEngineDeleteDispatch(options))。这一点是关键:若在这里手抄一个if (opts?.where?.id),每条用例都会写 rec_1,整套测试会把「早就修好了」报成绿。覆盖:
id与record.id不再互相矛盾;body id 指向一条不存在的行时,仍然写路径行(修复前这里回的是{ id: 'rec_1', record: null });If-Match放行,且放行的是路径行的写;过期 token 是CONCURRENT_UPDATE/ 409 且两行都没动;送另一行的版本号(正是 body 指向的那行)照样 409,currentVersion报的是路径行的 —— OCC 判的是路径行,现在守的也正是被写的那一行;RECORD_NOT_FOUND/ 404 且engine.update完全未被调用;无id的普通 PATCH、非标量 / 假值 body id(算子对象 / 数组 /null/0,即update的 **by-id** 路径同样把非标量data.id交给驱动写主键列(#6262 的孪生形状,where.id胜出时) #6435 那一族)、数组载荷,全都绑定路径行;undefined/null载荷仍然抛引擎自己的TypeError。最后一条刻意不按 ADR-0112 断言
code+status,差别正是要点:那不是本 ingress 新装的一条拒绝,而是ObjectQL.update对缺失载荷不做保护地读data.id的调用方编程错误,原样浮出来。报告里照实说明,而不是套模板凑一个信封断言。反向验证(方向先预测,再执行)
预测:撤掉修复后,只有「被写的行会变」的那几条转红,对照钉保持绿。
git checkout origin/main -- packages/metadata-protocol/src/protocol.ts后重跑该文件:4 红 / 12 绿,与预测一致 ——转绿的 12 条不是凑数:(a)、404、非标量 body id、
TypeError那几条在修复前后本来就该同样是绿,它们钉的是这次改动不许动的行为。本地跑过的
check:type-check-debt第一次在我自己批量脚本给的 3 GB 堆上 OOM 了,单独用 6 GB 重跑通过 —— 是我的资源设置,不是门禁判定。后继定价(PM 要的三张卡)
/meta列表):不受影响。那条走getMetaItems的读路径与 overlay 索引,与单条数据写 ingress 无交集。ensureOverlayIndexDROP-then-CREATE 窗口):不受影响。同一文件、不同区域(索引维护),本 PR 未触及;两处的临近只是文件级的,不是逻辑上的。id一定等于被写的行,少了一种「载荷说一行、被写的是另一行」的输入歧义要先澄清。仅此而已,不构成它的修复。范围外发现
无。
Generated by Claude Code