fix(cli): scaffolded controllers 404 on a missing or soft-deleted record - #3607
Merged
Merged
Conversation
show(), edit(), update() and delete() called findByKey bare. It returns a non-object for BOTH a missing key and a soft-deleted row (`deletedAt` set), so the action operated on an empty value and the view threw — a 500 where a 404 belongs. Reproduced on a soft-deleted post. The `crud` and `api-resource` generators already guard this; the scaffold was the outlier. A `requireRecord` BEFORE filter now throws Wheels.RecordNotFound, which the framework maps to 404. It deliberately does not assign the record: the actions keep their conventional single-finder body, which the `--belongsTo` parent wiring depends on. ScaffoldSource.showInclude() only rewrites show() when the finder is its entire body, so an inline guard there silently disables the include= wiring. Loading twice is cheap and preserves that contract. Why three earlier attempts failed, so nobody repeats them: ScaffoldSource.scan() fails CLOSED on any interpolated string — a single `#params.key#` anywhere in the file returns valid=false, method() then cannot find show(), and the parent wiring reports "skipped". The guard's message was interpolating the key. It is now a plain string, and the generated file must scan; a new assertion pins that at the source so this cannot regress quietly. The filter's finder also uses the all-named `findByKey(key=params.key)`: a spec forbids the bare positional form anywhere in the controller, since the mixed positional+named shape is rejected at runtime. Verified on a generated app: soft-deleted /posts/12 and nonexistent /posts/99999 -> 404 (both were 500); /posts/12/edit -> 404; real /posts/1 -> 200 with its eager-loaded comments still listed, proving the wiring survives. CLI suite: 1356 pass, the 4 pre-existing DbCommandSpec failures, 0 errors. Complexity gate: PASS. Signed-off-by: Peter Amiri <peter@alurium.com>
bpamiri
added a commit
that referenced
this pull request
Sep 13, 2026
…3608) #3607 added the requireRecord filter to templates/codegen/CRUDContent.txt and was verified on a generated app — but a STOCK `wheels new` app never got it. `wheels new` copies every codegen template into the app's app/snippets/, and Templates.cfc resolves those FIRST: they shadow the bundled copy. So the fix landed in the bundled template while the app-shipped twin silently overrode it with the old, guard-less body. Release 2487 carried the fix; a fresh app generated from 2487 did not. My verification had hand-written the rendered template into the controller, which is why it looked fixed. The snippet copy is now byte-identical to the bundled one. Confirmed on a genuinely stock app (fresh `wheels new`, real scaffold, real delete action, no edits): soft-deleted /posts/10 -> 404, /posts/99999 -> 404, /posts/10/edit -> 404, /posts/1 -> 200. A new spec pins the pair. Only this pair: two other snippet twins differ on purpose (the app copies read the reload password from .env), so a blanket "all snippets match codegen" rule would be wrong. 11 of 13 twins were already identical; this was the one that drifted. CLI suite: 1357 pass, the 4 pre-existing DbCommandSpec failures, 0 errors. Signed-off-by: Peter Amiri <peter@alurium.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.
show,edit,updateanddeletecalledfindByKeybare. It returns a non-object for both a missing key and a soft-deleted row (deletedAtset), so the action operated on an empty value and the view threw — a 500 where a 404 belongs. Reproduced on a soft-deleted post during the demo rehearsal. Thecrudandapi-resourcegenerators already guard this; the scaffold was the outlier.The fix
A
requireRecordbefore filter throwsWheels.RecordNotFound, which the framework maps to 404:filters(through="requireRecord", only="show,edit,update,delete"); private function requireRecord() { if (!IsObject(model("Post").findByKey(key=params.key))) { Throw(type = "Wheels.RecordNotFound", message = "Post not found for the requested key."); } }It deliberately does not assign the record. The actions keep their conventional single-finder body, which the
--belongsToparent wiring depends on —ScaffoldSource.showInclude()only rewritesshow()when the finder is its entire body, so an inline guard there silently disablesinclude=wiring. Loading twice is cheap and preserves that contract.Why the first three attempts failed
Worth recording so nobody repeats them.
ScaffoldSource.scan()fails closed on any interpolated string. A single#params.key#anywhere in the file returnsvalid=false;method()then can't findshow(), and the parent wiring reports "skipped". My guard's message was interpolating the key. It's now a plain string — and the new spec asserts the generated file scans, so this can't regress quietly.A spec forbids the bare positional
findByKey(params.key)anywhere in the controller (the mixed positional+named form is rejected at runtime). The filter uses the all-named form.Verified on a generated app
/posts/12(soft-deleted)/posts/99999(never existed)/posts/12/edit/posts/1(real)That last row is the one that proves the
--belongsTowiring survives.CLI suite: 1356 pass, the 4 pre-existing
DbCommandSpecfailures, 0 errors. Complexity gate: PASS.