fix(metadata-protocol): by-id bulk writes refuse a row that names no record (#5088) - #5098
Merged
Merged
Conversation
…record (#5088) `updateMany`, and `batch`'s `update` and `delete` branches, now answer RECORD_NOT_FOUND for a row whose id resolves to nothing — same code and same message as the single-record PATCH/DELETE have carried since #4435. #4435's per-row honesty had landed on 2 of the 5 write faces in this file: `updateData` (existence probe) and `runDeleteManyLoop` (`deleted === false`). The three bulk faces went straight to the engine: * `runUpdateManyLoop` / `runBatchDataLoop` update — no probe, so a stale id entered the WRITE PIPELINE. With no stored row to overlay, #4770's record materialisation produced a payload-only record, a hook condition reading any untouched field found it absent, and #4775's unevaluable-condition abort fired: the row failed INTERNAL_ERROR with a diagnostic accusing a correct hook of naming an undeclared field. Under `atomic` that row also took every later row to NOT_ATTEMPTED. * `runBatchDataLoop` delete — discarded the driver's return and pushed `success: true` unconditionally, so a batch of typo'd ids reported every one of them deleted. Verbatim the defect #4435 fixed in `deleteMany`, ten lines away. The gate is the existing `probeRecord`, extracted behind `assertRecordExists` so the by-id faces share ONE implementation (#4620's rule for this file): it asks EXISTENCE, not visibility, keeping the by-id write policy inside `engine.update` (#1994) and leaving the `rls-by-id-write` proof able to go red. Delete keeps the `=== false` reading — the contract's positive not-found value, never an inference from a falsy return. Unchanged on purpose: `upsert` (a missing id still inserts), the predicate bulk writes (`multi: true`, no per-row id — #4800/#4862's open design cards), the `atomic` response shape, and every row with a real id. Two existing fixtures needed the probe to be answerable rather than a stub (`findOne` returned null/undefined while nothing called it); the batch-row conformance pin now asserts the catalogued 404 where it asserted an unclassified engine throw. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BotUP49pqhvqGY393n2HfU
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Contributor
Author
范围外发现(已另行开单,未在本 PR 修改)按 Prime Directive #10 登记,两条都在本次改动读到的同一个循环里,但都不在 #5088 的验收面内:
本 PR 严格只覆盖 PM 分诊列出的三个写面( Generated by Claude Code |
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 #5088
问题
updateMany对一个不存在的 id 不做任何存在性检查,直接把该行送进engine.update。写管线里没有可供叠加的存量记录,#4770 的记录物化(stored ⊕ payload)只剩 payload 一侧,hook 的condition读到任何本次未写入的字段都判为缺失,于是 #4775 的「条件不可求值即中止」触发 —— 该行以INTERNAL_ERROR失败,诊断信息还反过来指控一个正确的 hook 引用了对象未声明的字段。操作者批量更新时带了一个过期 id,被告知的却是「你的 hook 坏了」,并被指向对象的字段清单;atomic: true下这一行还会把整批拖成NOT_ATTEMPTED,一个陈旧 id 读起来就像平台故障。按 PM 分诊复核,缺陷面比 issue 正文更宽 —— #4435 的「按行诚实」只落在本文件 5 个写面中的 2 个:
runUpdateManyLooprunBatchDataLoop的update分支runBatchDataLoop的delete分支engine.delete返回值,无条件success: true—— 一批 typo 的 id 全部回报删除成功,与 #4435 正文描述的原始缺陷逐字同形runDeleteManyLoopdeleted === false)updateDataprobeRecord先探再写)修法
三个 by-id 写面补齐同一道闸门,恢复既有不变量,不引入新契约:
assertRecordExists(object, id),内部就是单记录路径那只probeRecord。按 fix(metadata-protocol): deleteManyData has the same fake-atomic as batchData, updateManyData ignores atomic entirely #4620 对本文件的要求,by-id 写面共用一份实现,而不是「今天恰好一致的三份」。engine.update内的 fix(security)[P0]: enforce RLS on by-id writes — close member-edits-others'-records hole (#1985) #1994 前像检查里,rls-by-id-write证明依然能报红。同样地,探针在写之前,不从 readback 为 null 反推 not-found ——updateData的注释已说明后者会把一次成功的写(把owner_id改到自己作用域之外)误判成 404。engine.transaction在 ambienttxStore里执行回调(ADR-0034),buildDriverOptions在上下文没有显式transaction时回落到该 store,所以探针与它所守护的写走同一条连接。runDeleteManyLoop的=== false读法 —— 契约(IDataDriver.delete)的 positive not-found 值,不从 falsy 返回值推断,以免把「返回被删行」的第三方 driver 变成假 404。刻意不动:
upsert(missing id 仍然插入)、predicate 批量写(multi: true,无逐行 id —— 属 #4800 / #4862 的未决设计卡)、atomic的响应形状(causal row 位置不变,其后仍NOT_ATTEMPTED)、以及所有真实 id 的行为。验收对照
updateMany不存在 id 的行 →errors[0].code === 'RECORD_NOT_FOUND',httpStatus: 404,消息与单记录路径逐字一致(Record {id} not found in {object},花括号处为实际 id / 对象名);测试直接断言三条路径产出同一条消息。engine.update对该行从未被调用(不只是响应码对)。atomic: true下该行为 causal row,其余NOT_ATTEMPTED;先行成功的行读回存储确认已回滚。batchData的 update / delete 分支同步补齐;整批 typo 的 id 现在succeeded: 0。succeeded计数、droppedFields、returnRecords: false的裁剪、context 透传均不变。测试
新增
packages/metadata-protocol/src/protocol.bulk-record-not-found.test.ts(15 例,三个写面 × 非 atomic/atomic + 回归)。fake engine 对未知 id 的写逐字复现 #4775 的中止信息,所以修复前的红是 issue 里那条INTERNAL_ERROR,而不是某个抽象的断言差异。先证红再信绿 —— 只回退
protocol.ts、保留测试:修复后全绿:
Test Files 37 passed (37) / Tests 334 passed (334)(@objectstack/metadata-protocol),@objectstack/rest577 passed (577),两包typecheck均通过(--force,无缓存)。两处既有 fixture 需要同步:
protocol.dropped-fields.bulk.test.ts的findOne是个「什么都不存在」的空桩(此前无人调用它),现在它就是写前的存在性探针,必须对它自称要更新的行给出答案;protocol.batch-row-conformance.test.ts里那条断言从未分类的引擎抛错(no such record)改为断言目录化的 404 —— 那正是本 PR 要产生的改进。备注
高频调用方需知:这三个写面的每个 by-id 行,现在写之前多一次存在性读。这与单记录路径自 #4435 起接受的代价同形,已写入 changeset。
🤖 Generated with Claude Code
https://claude.ai/code/session_01BotUP49pqhvqGY393n2HfU