intent: a forbidWhen refuses the DELETE of the row it guards (#7372) - #7417
Merged
Merged
Conversation
`checks: forbidWhen` refused a create and an update while its condition held, and its master guard hides the child panel's Add, row edit AND row delete - but the server half covered only the first two. A rule reading "no line may change on a sent quotation" therefore let the line be REMOVED, the largest of the three changes, through anything that is not the generated page. The hidden affordance is what made it silent: a reviewer clicking through the pages sees all three gone and concludes the rule holds. forbidWhen is the only check kind about the write HAPPENING rather than about the values it carries, so it is the only one with anything to say about a delete: `splitChecks` puts every forbidWhen into `deleteChecks`, and the three generated controllers emit `requireDeletable(<Name>Entity)` from it and call it on their delete verb with the STORED row, since a delete carries no payload. The gate rides along on that verb instead of routing the rule to the repository as it does for the write half: a delete is nobody's transition - no process step deletes a record, `whenDeleted:` only reacts to one - so there is no repository-side write for a gated check to sit on. Keeping it off the repository is also what leaves the composition cascade alone: a master sweeping its own children away goes through their repositories, and whether THAT delete is allowed is what `whenMasterDeleted:` declares. Also fixes the same construct's message interpolation on the three write sites it already had: the raw `message` was written into a Java literal instead of its escaped twin, so an authored message quoting a field name - which the DSL's own examples suggest - failed the compile of the whole module's generated code (#7241). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What was wrong
checks: forbidWhen(#7275) refused a create and an update while its condition held, and its master guard hides the child panel's Add, row edit AND row delete — but the server half covered only the first two:So a rule reading "no line may change on a sent quotation" let the line be removed — the largest of the three changes — through anything that is not the generated page. The hidden affordance is what made it silent: a reviewer clicking through the pages sees all three gone and concludes the rule holds.
The fix
forbidWhenis the only check kind about the write happening rather than about the values it carries, so it is the only one with anything to say about a delete.ModelParameterProcessor.splitChecksputs everyforbidWheninto a newdeleteCheckslist (requiredWhen,compare,exactlyOne,itemsMin,itemsSumEqualreach it never), and all three generated controllers emitrequireDeletable(<Name>Entity entity)from it and call it on their delete verb with the stored row, since a delete carries no payload. An entity with noforbidWhengenerates byte-identically.The gate rides along on that verb rather than routing the rule to the repository as it does for the write half, which deviates slightly from the issue's expectation and is deliberate:
whenDeleted:only reacts to one — so there is no repository-side write for a gated check to sit on;delete(entity). Deleting a SENT quotation would then be refused by its own guarded lines — and whether that delete is allowed is whatwhenMasterDeleted:declares, not a child check's business.Every delete a person performs arrives over REST, so the three controllers are where the gap actually closes — the same place
immutableWhen/locksWithMasteralready enforce theirs.Also fixed while in the same branch of the same construct: its three existing write sites wrote the raw
messageinto a Java string literal instead of the escaped twin, so an authored message quoting a field name — which the DSL's own examples suggest — failed the compile of the whole module's generated code (#7241). NowmessageJavaLiteraleverywhere.Verified
ModelParameterProcessorTest— 57/57 green, including the extended split assertion and a newnoOtherCheckKindReachesTheDeleteList.ForbidWhenDeleteTemplateIT— renders all three controller templates through the platform's own Velocity engine: the guard, its call site on each surface, the gated variant carrying its status condition, an unguarded entity emitting nothing at all, the escaped message, and no unresolved${...}.IntentEmissionCoverageIT— green (333 s), and extended end-to-end rather than only at the source level: a newCampaignTaskfixture carries the ungatedforbidWhenwithlocksWithMaster: false, so the inherited 409 is out of the way and a 400 can only be the check. The task created before the campaign closed is the control; after it closes, POST / PUT / DELETE are each a 400 with the authored message and the row is still readable afterwards. Plus the emitted-source assertions onEntryLineController(guard + call site) and onDocController(arequiredWhenemits no delete guard).mvn formatter:validateover the whole reactor with the formatter cache wiped — BUILD SUCCESS.Not run: the rest of the integration suite, and the full unit suite outside
ide-template.Fixes #7372
🤖 Generated with Claude Code