Skip to content

Commit 7da353a

Browse files
fix(cli): generate api-resource resolves objectName placeholders (wheels-dev#2566)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent f6c2fbc commit 7da353a

3 files changed

Lines changed: 78 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
152152

153153
### Fixed
154154

155+
- `wheels generate api-resource` now produces a controller with resolved identifiers instead of literal `#objectNamePlural#` / `#objectNameSingular#` placeholders. The framework snippet at `app/snippets/ApiControllerContent.txt` was still using the legacy hash-token form that the CLI's `Templates.processTemplate()` does not substitute, while the CLI-bundled copy already used the pipe-delimited `|ObjectNamePlural|` / `|ObjectNameSingular|` tokens it understands. Aligned the framework-level snippet with the CLI-bundled one. (#2468)
155156
- Framework dev pages (`/wheels/guides`, `/wheels/info`, `/wheels/migrator`, `/wheels/packages`, error screens) now render Semantic UI icons instead of empty bordered squares. The dev layouts inline `semantic.min.css` into a `<style>` block, so its relative URLs to `themes/default/assets/fonts/icons.woff2` resolved against the page URL and 404'd — every `<i class="...icon">` rendered as the fallback square. `_header.cfm` and `_header_simple.cfm` now read the woff2 once at application scope, base64-encode it, and emit a `@font-face` override after the inlined Semantic CSS. Initialization uses double-checked locking on `application.wheels.iconsFontDataUri` so concurrent first-requests can't read an intermediate empty value. (#2563)
156157
- Debug bar Tools → Packages page now lists packages available from the `wheels-dev/wheels-packages` registry in fresh apps generated with `wheels new`. The previous gate (`FileExists("/cli/lucli/services/packages/Registry.cfc")`) silently returned an empty list because user apps don't ship the CLI alongside the framework. The registry reader now lives at `vendor/wheels/services/packages/{Registry,HttpClient,ManifestCache}.cfc` and ships with every generated app. The registry list stays scoped to the standalone Tools → Packages page; the inline debug-bar Environment panel shows installed packages only, so the bar stays compact and doesn't trigger a registry walk on every dev-mode page load. (#2530)
157158
- `Registry.fetchManifest()` now validates that a manifest contains a non-empty `versions` array before returning, throwing `Wheels.Packages.RegistryMalformed` instead of letting a downstream `local.m.versions[ArrayLen(...)]` access crash with an unhandled `Expression` error. The per-package skip-on-malformed catch in `listAll()` now actually catches every malformed shape, so the Tools → Packages page degrades gracefully when the registry serves a partial manifest. Mirrored into the CLI's `cli/lucli/services/packages/Registry.cfc` to keep both copies in sync. (#2530)

app/snippets/ApiControllerContent.txt

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,71 +6,71 @@
66
}
77

88
/**
9-
* GET /#objectNamePlural#
10-
* Returns a list of all #objectNamePlural#
9+
* GET /|ObjectNamePlural|
10+
* Returns a list of all |ObjectNamePlural|
1111
*/
1212
function index() {
13-
local.#objectNamePlural# = model("|ObjectNameSingular|").findAll();
14-
renderWith(data={ #objectNamePlural#=local.#objectNamePlural# });
13+
local.|ObjectNamePlural| = model("|ObjectNameSingular|").findAll();
14+
renderWith(data={ |ObjectNamePlural|=local.|ObjectNamePlural| });
1515
}
1616

1717
/**
18-
* GET /#objectNamePlural#/:key
19-
* Returns a specific #objectNameSingular# by ID
18+
* GET /|ObjectNamePlural|/:key
19+
* Returns a specific |ObjectNameSingular| by ID
2020
*/
2121
function show() {
22-
local.#objectNameSingular# = model("|ObjectNameSingular|").findByKey(params.key);
22+
local.|ObjectNameSingular| = model("|ObjectNameSingular|").findByKey(params.key);
2323

24-
if (IsObject(local.#objectNameSingular#)) {
25-
renderWith(data={ #objectNameSingular#=local.#objectNameSingular# });
24+
if (IsObject(local.|ObjectNameSingular|)) {
25+
renderWith(data={ |ObjectNameSingular|=local.|ObjectNameSingular| });
2626
} else {
2727
renderWith(data={ error="Record not found" }, status=404);
2828
}
2929
}
3030

3131
/**
32-
* POST /#objectNamePlural#
33-
* Creates a new #objectNameSingular#
32+
* POST /|ObjectNamePlural|
33+
* Creates a new |ObjectNameSingular|
3434
*/
3535
function create() {
36-
local.#objectNameSingular# = model("|ObjectNameSingular|").new(params.#objectNameSingular#);
36+
local.|ObjectNameSingular| = model("|ObjectNameSingular|").new(params.|ObjectNameSingular|);
3737

38-
if (local.#objectNameSingular#.save()) {
39-
renderWith(data={ #objectNameSingular#=local.#objectNameSingular# }, status=201);
38+
if (local.|ObjectNameSingular|.save()) {
39+
renderWith(data={ |ObjectNameSingular|=local.|ObjectNameSingular| }, status=201);
4040
} else {
41-
renderWith(data={ error="Validation failed", errors=local.#objectNameSingular#.allErrors() }, status=422);
41+
renderWith(data={ error="Validation failed", errors=local.|ObjectNameSingular|.allErrors() }, status=422);
4242
}
4343
}
4444

4545
/**
46-
* PUT /#objectNamePlural#/:key
47-
* Updates an existing #objectNameSingular#
46+
* PUT /|ObjectNamePlural|/:key
47+
* Updates an existing |ObjectNameSingular|
4848
*/
4949
function update() {
50-
local.#objectNameSingular# = model("|ObjectNameSingular|").findByKey(params.key);
50+
local.|ObjectNameSingular| = model("|ObjectNameSingular|").findByKey(params.key);
5151

52-
if (IsObject(local.#objectNameSingular#)) {
53-
local.#objectNameSingular#.update(params.#objectNameSingular#);
52+
if (IsObject(local.|ObjectNameSingular|)) {
53+
local.|ObjectNameSingular|.update(params.|ObjectNameSingular|);
5454

55-
if (local.#objectNameSingular#.hasErrors()) {
56-
renderWith(data={ error="Validation failed", errors=local.#objectNameSingular#.allErrors() }, status=422);
55+
if (local.|ObjectNameSingular|.hasErrors()) {
56+
renderWith(data={ error="Validation failed", errors=local.|ObjectNameSingular|.allErrors() }, status=422);
5757
} else {
58-
renderWith(data={ #objectNameSingular#=local.#objectNameSingular# });
58+
renderWith(data={ |ObjectNameSingular|=local.|ObjectNameSingular| });
5959
}
6060
} else {
6161
renderWith(data={ error="Record not found" }, status=404);
6262
}
6363
}
6464

6565
/**
66-
* DELETE /#objectNamePlural#/:key
67-
* Deletes a #objectNameSingular#
66+
* DELETE /|ObjectNamePlural|/:key
67+
* Deletes a |ObjectNameSingular|
6868
*/
6969
function delete() {
70-
local.#objectNameSingular# = model("|ObjectNameSingular|").findByKey(params.key);
70+
local.|ObjectNameSingular| = model("|ObjectNameSingular|").findByKey(params.key);
7171

72-
if (IsObject(local.#objectNameSingular#)) {
73-
local.#objectNameSingular#.delete();
72+
if (IsObject(local.|ObjectNameSingular|)) {
73+
local.|ObjectNameSingular|.delete();
7474
renderWith(data={}, status=204);
7575
} else {
7676
renderWith(data={ error="Record not found" }, status=404);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Regression: app/snippets/ApiControllerContent.txt is consumed by the CLI
3+
* Templates service (cli/lucli/services/Templates.cfc) when `wheels generate
4+
* api-resource` is run. The processor only replaces pipe-delimited tokens
5+
* (e.g. |ObjectNameSingular|, |ObjectNamePlural|); the legacy ##objectName##
6+
* style is NOT substituted and lands verbatim in generated controllers.
7+
*
8+
* Issue #2468: the framework snippet still uses the legacy ##objectName##
9+
* tokens, so generated API controllers contain unresolved placeholders.
10+
*/
11+
component extends="wheels.WheelsTest" {
12+
13+
function run() {
14+
15+
describe("app/snippets/ApiControllerContent.txt", function() {
16+
17+
it("uses pipe-delimited tokens the CLI Templates processor understands", function() {
18+
var path = expandPath("/app/snippets/ApiControllerContent.txt");
19+
expect(fileExists(path)).toBeTrue("Snippet missing at " & path);
20+
21+
var content = fileRead(path);
22+
23+
// Hash literals must be doubled in CFML strings — these strings
24+
// represent the legacy, unresolved token forms.
25+
var legacyPlural = "##" & "objectNamePlural" & "##";
26+
var legacySingular = "##" & "objectNameSingular" & "##";
27+
28+
expect(content contains legacyPlural).toBeFalse(
29+
"Snippet still contains legacy token " & legacyPlural
30+
& " — CLI Templates.processTemplate() doesn't substitute this form."
31+
);
32+
expect(content contains legacySingular).toBeFalse(
33+
"Snippet still contains legacy token " & legacySingular
34+
& " — CLI Templates.processTemplate() doesn't substitute this form."
35+
);
36+
37+
expect(content contains "|ObjectNamePlural|").toBeTrue(
38+
"Snippet should use |ObjectNamePlural| — the token Templates.processTemplate() replaces."
39+
);
40+
expect(content contains "|ObjectNameSingular|").toBeTrue(
41+
"Snippet should use |ObjectNameSingular| — the token Templates.processTemplate() replaces."
42+
);
43+
});
44+
45+
});
46+
47+
}
48+
49+
}

0 commit comments

Comments
 (0)