diff --git a/cli/lucli/templates/app/app/snippets/CRUDContent.txt b/cli/lucli/templates/app/app/snippets/CRUDContent.txt index e034e6e03..fa83f15d3 100644 --- a/cli/lucli/templates/app/app/snippets/CRUDContent.txt +++ b/cli/lucli/templates/app/app/snippets/CRUDContent.txt @@ -8,6 +8,17 @@ */ function config() { super.config(); + // 404 rather than a 500 when the key matches nothing. findByKey returns a + // non-object for BOTH a missing key and a soft-deleted row, and the + // actions below then operate on that empty value — `post.title` in the + // view, or `.update()` here, throws. + // + // A BEFORE filter that deliberately does NOT assign the record: the + // actions keep their conventional single-finder body, which the + // `--belongsTo` parent wiring depends on — it only rewrites `show()` + // when the finder is the whole body, and skips a customized one. + // Loading twice is cheap and keeps that contract intact. + filters(through="requireRecord", only="show,edit,update,delete"); } /** @@ -81,4 +92,21 @@ redirectTo(route="|ObjectNamePlural|"); } + /** + * Guard: abort with a 404 when the key matches no record. + * + * The message is a plain string on purpose. The scaffold's parent wiring + * scans this file with ScaffoldSource, which refuses interpolated strings + * (`##...##`) wholesale — a single `##params.key##` here would silently turn + * off `include=` wiring for every `--belongsTo` child of this model. + **/ + private function requireRecord() { + if (!IsObject(model("|ObjectNameSingularC|").findByKey(key=params.key))) { + Throw( + type = "Wheels.RecordNotFound", + message = "|ObjectNameSingularC| not found for the requested key." + ); + } + } + } diff --git a/cli/lucli/tests/specs/commands/NewCommandTemplateSpec.cfc b/cli/lucli/tests/specs/commands/NewCommandTemplateSpec.cfc index a7792e687..36e5a52ec 100644 --- a/cli/lucli/tests/specs/commands/NewCommandTemplateSpec.cfc +++ b/cli/lucli/tests/specs/commands/NewCommandTemplateSpec.cfc @@ -129,6 +129,25 @@ component extends="wheels.wheelstest.system.BaseSpec" { expect(fileExists(templateRoot & "tests/specs/models/.gitkeep")).toBeTrue(); }); + it("ships app/snippets/CRUDContent.txt identical to the bundled codegen template", () => { + // `wheels new` copies every codegen template into the app's + // app/snippets/, and Templates.cfc resolves THOSE first — they + // shadow the bundled copy. So a fix to templates/codegen/ + // CRUDContent.txt is invisible to every freshly generated app + // unless the snippet copy moves with it. That is exactly how the + // 404 guard shipped in a release and then failed to appear in a + // stock `wheels new` app: the two files had silently diverged. + // + // Only this pair is pinned. Two other twins differ on purpose + // (the app copies read the reload password from .env), so a + // blanket "all snippets match codegen" rule would be wrong. + var bundled = fileRead(expandPath("/cli/lucli/templates/codegen/CRUDContent.txt")); + var shipped = fileRead(templateRoot & "app/snippets/CRUDContent.txt"); + expect(compare(shipped, bundled)).toBe(0); + // And the shipped copy must actually carry the guard. + expect(shipped).toInclude('filters(through="requireRecord"'); + }); + }); }