fix(metadata): 明细行随父记录级联删除(opportunity / quote) - #1006
Merged
Conversation
…deleted Both line-item objects hang off their parent through a REQUIRED lookup that declared no `deleteBehavior`. That resolves to the spec default `set_null`, and the engine escalates a `set_null` default on a required lookup to `restrict` (a NOT NULL column cannot be cleared) — so every itemised opportunity or quote refused to delete with a 409 DELETE_RESTRICTED whose message told the API caller to set `deleteBehavior:'cascade'` on a metadata key they cannot reach. Declare `deleteBehavior: 'cascade'` on `crm_opportunity_line_item .crm_opportunity` and `crm_quote_line_item.crm_quote`. A line item is subordinate by construction: both objects state a line has no meaning apart from its parent, and the rollup hooks derive the parent's amount / totals FROM the line set. Scope is exactly those two fields. The `crm_product` lookup on both objects stays on the restricting default so retiring a catalog product cannot shred priced history, and the campaign/event parent lookups (#696, #711) keep restricting for the same historical-record reason. `test/line-item-cascade.test.ts` pins the resolved declarations and runs the deletes end-to-end on a real ObjectQL + InMemoryDriver. Fixes #727
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
August 7, 2026 02:05
This was referenced Aug 7, 2026
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 #727
现象
在 fresh
origin/main+@objectstack/* 17.0.0-rc.3上复验,issue 前提依然成立:只要商机/报价挂过明细行(即用产品目录配过价的正常状态),父记录就删不掉,且拒绝文案把一条调用方根本够不着的元数据键甩给了 API 使用者:crm_quote→crm_quote_line_item是同一句话换名词。唯一的绕行是先手工把明细行一条条删干净。根因
不是引擎缺陷,是一个没人写下来的默认值。
crm_opportunity_line_item.crm_opportunity与crm_quote_line_item.crm_quote都没声明deleteBehavior。「不声明」不等于「没意见」:spec 把该字段落到默认值set_null(rc.3 实测,解析后的字段字面量就是"deleteBehavior":"set_null",而源码里一个字都没写),而引擎的cascadeDeleteRelations会把 required lookup 上的set_null默认升级成restrict—— NOT NULL 列没法置空。所以两个对象的源码里没有任何一处表达过「禁止删除父记录」,这个行为完全来自那条隐式默认。契约核验(改之前的硬前置)
在动手前先确认 rc.3 契约支持这个键,而不是写平台 workaround:
node_modules/@objectstack/spec/src/data/field.zod.ts:463deleteBehavior: z.enum(['set_null', 'cascade', 'restrict']).optional().default('set_null')—— 键存在,取值面含
'cascade'。node_modules/@objectstack/objectql/dist/core.js:6541behavior = fdef.type === "master_detail" ? … : fdef.deleteBehavior || "set_null",随后if (behavior === "set_null" && fdef.required === true) behavior = "restrict";behavior === "cascade"分支逐条this.delete(childName, …)。—— 引擎确实消费它,且级联发生在父记录
driver.delete之前(core.js:6626)。契约支持,故按 issue 的方案一实施。
修法
两个父 lookup 各加一行
deleteBehavior: 'cascade',并把「为什么是 cascade、为什么不是 restrict」写进对象源码。明细行在构造上就是从属记录,这一点两个对象本来就说了两遍:对象头注释写着 line item 离开它的 deal 就没有意义,而 rollup hook 把crm_opportunity.amount和报价的 subtotal/total 都是从明细行集合派生出来的。父记录没了还留着的明细行什么都不指代,只会让一个已删除商机的收入继续活在所有行级报表里。范围严格限定在这两个字段,并且把边界也一并钉住:
crm_productlookup 保持在会升级成 restrict 的默认上 —— 下架一个产品目录项不能连带铲掉卖过它的成交历史(product.hook.ts的beforeDelete也是同一句话:Set is_active=false to retire instead)。crm_campaign_member.crm_campaign和crm_event_attendee.crm_event继续 restrict,因为活动的成员名单、会议的参与者名单是那条记录本身的历史证据,而一条价格行不是。两个方向都有断言兜着,任一边的复制粘贴都会红。验证
新增
test/line-item-cascade.test.ts,沿用仓内已有的*-cascade.test.ts家族形态(结构断言 + 真引擎端到端放在同一个文件,理由见campaign-member-cascade.test.ts的说明):结构上钉住两个父 lookup 解析值为cascade、两个 product lookup 仍为set_null;行为上在ObjectQL+InMemoryDriver上跑真删除 —— 挂明细的商机/报价删得掉且明细随删、别的 deal 的行和同产品的报价行都不受影响、没挂明细的商机照常删、引用中的产品仍被 restrict 拦住并报出真正的阻塞方。反向验证(先定方向再跑):预期把两行声明摘掉后 2 条结构断言 + 2 条端到端删除转红,实测
5 failed | 4 passed—— 多红的一条是跨 deal 隔离用例(它同样依赖级联真的发生),4 条绿的正是不依赖级联的边界断言。方向与预期一致,非空转通过。六门:
changeset:
.changeset/line-item-cascade-on-parent-delete.md(patch),正文按发布说明读者写清了「删商机/报价会连带删掉产品行、行级收入报表相应下降」这个用户可见后果。未做的事
没有改
content/docs/—— 同族的 #712(campaign_member 级联)也没有配套文档改动,本次行为变更的用户可见说明走 changeset。若维护者认为应当在sales/opportunities.mdx/sales/quotes.mdx三个语言版本各补一条删除语义说明,可另开单,我不在本单里顺手加。Generated by Claude Code