Skip to content

Commit 7fae2a3

Browse files
bpamiriclaude
andauthored
docs(web/guides): fix basics + digging-deeper guide audit findings (group 5, un-gated) (#3130)
* docs(web/guides): fix basics + digging-deeper guide audit findings (un-gated group 5) Behavioral-audit corrections, all source- or probe-verified: basics/ - models-and-the-orm: correct the callbacks contract — afterFind on query-returning finders receives row columns as arguments and decorates by returning a struct; 'must be private' softened to the actual convention (public works but leaks onto the model API) - associations: caution that hasMany shortcut= is currently broken (Wheels.AssociationNotFound on every advertised surface, #3109) - validation-and-errors: drop the nonexistent validatesAssociated helper; point at nested-properties validation instead - forms-and-form-helpers: checkBox hidden companion renders AFTER the box under name '<name>($checkbox)' and the dispatcher applies uncheckedValue only when the real field is absent; labelPlacement default is 'around', not 'before' - index: replace the Phase 2 placeholder with a real section landing page linking all 12 content pages digging-deeper/ - background-jobs: rewrite worker/monitor/retry/purge sections around the programmatic surface that exists (processQueue, queueStats, retryFailed, purgeCompleted) — there is no 'wheels jobs' CLI (#3090) - route-model-binding: silent skip on missing model applies only to conventional binding; explicit binding="Name" rethrows (#3118) - dependency-injection-usage: singleton cache is keyed by alias (not component path) and names are case-insensitive (#3117) - authentication-patterns: parenthesize the elvis in the format check (unparenthesized form can never reach the JSON branch, #3116); fix the hasStrategy rationale (registerStrategy replaces same-name entries; duplicates cannot stack) verify:docs exit 0 on every touched page; guides site builds clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): correct afterFind argument contract and job backoff retry count Review findings on #3130: - models-and-the-orm.mdx: object-mode afterFind callbacks DO receive the record's properties as named arguments ($afterFindCallback invokes with invokeArgs = properties()) and a returned struct is applied back via setProperties(). Both finder modes now documented as receiving named arguments and honoring a returned struct; the dual-branch StructKeyExists example replaced with the portable return-a-struct pattern. - background-jobs.mdx: maxRetries counts total attempts (currentAttempts < maxRetries in Job.cfc), so maxRetries=5 yields the first run plus four retries at 4s/8s/16s/32s — the 64s retry never fires. Reframed around attempts vs retries and expanded the maxRetries=3 default parenthetical (retries at 4s and 8s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5bddcd1 commit 7fae2a3

9 files changed

Lines changed: 198 additions & 66 deletions

File tree

web/sites/guides/src/content/docs/v4-0-0/basics/associations.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ Include multiple associations by comma-separating: `include="comments,author"`.
229229

230230
## Many-to-many (through a join model)
231231

232+
<Aside type="caution">
233+
**`shortcut` is currently broken in 4.0.x.** Declaring `hasMany(name="...", shortcut="...")` poisons the association's expansion: the shortcut method, the plain join-rows method, *and* `findAll(include=...)` for that association all throw `Wheels.AssociationNotFound` at runtime. Track the fix in [#3109](https://github.com/wheels-dev/wheels/issues/3109). Until it lands, declare the plain `hasMany` join-model associations (no `shortcut`) and traverse the join model explicitly — `user.userRoles(include="role")` — or query the far side with an explicit join.
234+
</Aside>
235+
232236
Wheels handles many-to-many through a real join model plus the `shortcut` argument on `hasMany`. If a User has many Roles via a UserRole join table, model the relationship as three classes:
233237

234238
```cfm {test:compile}

web/sites/guides/src/content/docs/v4-0-0/basics/forms-and-form-helpers.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Every helper also accepts the standard Wheels form options — `label`, `labelPl
102102
See `vendor/wheels/view/formsobject.cfc` for the full argument surface.
103103

104104
<Aside type="note">
105-
**`checkBox` emits two inputs.** A single checkbox only submits its `name` when it's checked, which means unchecking a box sends no value at all and the controller never learns the user cleared it. `checkBox` works around this by also rendering a hidden `<input type="hidden" name="..." value="0">` just before the real checkbox. On submit, the last value wins: checked → `1`, unchecked → `0`. The underlying model always sees one of the two.
105+
**`checkBox` emits two inputs.** A single checkbox only submits its `name` when it's checked, which means unchecking a box sends no value at all and the controller never learns the user cleared it. `checkBox` works around this by also rendering a hidden companion *after* the real checkbox, under a marker name — `<input type="hidden" name="post[published]($checkbox)" value="0">`. On submit, Wheels' dispatcher detects the `($checkbox)` key and applies its `uncheckedValue` only when the real field is absent from the form post: checked → `1`, unchecked → `0`. The underlying model always sees one of the two.
106106
</Aside>
107107

108108
<Aside type="note">
@@ -162,7 +162,7 @@ The summary block only renders when the object has errors — on a new-form rend
162162

163163
Wheels does not ship a standalone `label()` helper. Labels are a first-class argument on every object-bound form helper via `label=` and `labelPlacement=`:
164164

165-
- `textField(objectName="post", property="title", label="Post title")` — wraps the input in a `<label for="post-title">Post title</label>` with `labelPlacement` controlling whether the text comes `before` (default), `after`, or `around` the input.
165+
- `textField(objectName="post", property="title", label="Post title")` — wraps the input in a `<label for="post-title">Post title</label>` with `labelPlacement` controlling whether the text comes `around` (default), `before`, or `after` the input.
166166
- `label=false` suppresses the auto-label — use this when you want to write the `<label>` tag by hand (for icons, extra classes, or explicit `for` attributes).
167167

168168
The third and most common pattern is in the canonical example above: skip the `label` argument entirely and wrap the helper call inside a raw `<label>` element in your markup. The browser associates the label with the input via containment, no `for="..."` attribute needed.

web/sites/guides/src/content/docs/v4-0-0/basics/index.mdx

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,85 @@ sidebar:
66
order: 3
77
---
88

9-
Placeholder — content lands in Phase 2.
9+
import { CardGrid, LinkCard } from '@astrojs/starlight/components';
10+
11+
The Basics covers the everyday surface of a Wheels app — the routing, controller, view, and model work you'll do on every feature. Each page is a task-oriented how-to. They're ordered to follow a request through the framework, but every page stands alone — jump to whichever matches what you're building right now.
12+
13+
**You'll find:**
14+
15+
- Routing, controllers, and the seven REST actions
16+
- Views, layouts, partials, and every form helper
17+
- Validations and rendering errors back to the user
18+
- Models, finders, associations, and the chainable query builder
19+
- Migrations, seeding, and multi-datasource setups
20+
21+
## All Basics guides
22+
23+
<CardGrid>
24+
<LinkCard
25+
title="Routing"
26+
href="/v4-0-0/basics/routing/"
27+
description="Define routes — resources, nested resources, namespaced routes, custom patterns, and route helpers."
28+
/>
29+
<LinkCard
30+
title="Controllers and Actions"
31+
href="/v4-0-0/basics/controllers-and-actions/"
32+
description="Writing a controller, the seven REST actions, filters, params, rendering, redirect, and flash."
33+
/>
34+
<LinkCard
35+
title="Views, Layouts, Partials"
36+
href="/v4-0-0/basics/views-layouts-partials/"
37+
description="Rendering templates, the default layout, extracting partials, and how controller data reaches the view."
38+
/>
39+
<LinkCard
40+
title="Forms and Form Helpers"
41+
href="/v4-0-0/basics/forms-and-form-helpers/"
42+
description="Every form helper Wheels ships — object-bound and tag-style — plus data-auto-id for stable test selectors."
43+
/>
44+
<LinkCard
45+
title="Validation and Error Display"
46+
href="/v4-0-0/basics/validation-and-errors/"
47+
description="Built-in validations, when they fire, the errors API, and rendering errors inline."
48+
/>
49+
<LinkCard
50+
title="Models and the ORM"
51+
href="/v4-0-0/basics/models-and-the-orm/"
52+
description="Defining a model, finders, persistence, and lifecycle callbacks — the everyday ORM surface."
53+
/>
54+
<LinkCard
55+
title="Associations"
56+
href="/v4-0-0/basics/associations/"
57+
description="Modeling relationships — hasMany, belongsTo, hasOne, nested creation, eager loading, cascade deletes."
58+
/>
59+
<LinkCard
60+
title="Migrations"
61+
href="/v4-0-0/basics/migrations/"
62+
description="Writing and running schema migrations — column types, indexes, foreign keys, and rollbacks."
63+
/>
64+
<LinkCard
65+
title="Seeding"
66+
href="/v4-0-0/basics/seeding/"
67+
description="Populate development and production databases with default records — idempotently."
68+
/>
69+
<LinkCard
70+
title="Query Builder and Scopes"
71+
href="/v4-0-0/basics/query-builder-and-scopes/"
72+
description="Chainable query builder, reusable scopes, enum declarations, and batch processing."
73+
/>
74+
<LinkCard
75+
title="Database and Multiple Datasources"
76+
href="/v4-0-0/basics/database-and-multiple-datasources/"
77+
description="Configuring the default datasource, per-model overrides, transactions, and raw queries."
78+
/>
79+
<LinkCard
80+
title="Shared Development Databases"
81+
href="/v4-0-0/basics/shared-development-databases/"
82+
description="Reconciling the migration tracking table when several developers share one dev database."
83+
/>
84+
</CardGrid>
85+
86+
## See also
87+
88+
- [Start Here](/v4-0-0/start-here/) — install Wheels and build your first app with the tutorial
89+
- [Core Concepts](/v4-0-0/core-concepts/) — the explanations behind what these guides show you how to do
90+
- [Digging Deeper](/v4-0-0/digging-deeper/) — authentication, jobs, caching, uploads, and the rest of the advanced surface

web/sites/guides/src/content/docs/v4-0-0/basics/models-and-the-orm.mdx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ For a key-only delete without loading the record, use `model("Post").deleteByKey
162162

163163
## Lifecycle callbacks
164164

165-
Callbacks register a `private` method to run at a specific point in the record's lifecycle. The method takes no arguments (the record is `this`), returns nothing, and can mutate any property on `this` before the write happens. Return `false` from a `before*` callback to cancel the operation.
165+
Callbacks register a method to run at a specific point in the record's lifecycle. For the write-cycle callbacks the method takes no arguments (the record is `this`), returns nothing, and can mutate any property on `this` before the write happens. Return `false` from a `before*` callback to cancel the operation. (`afterFind` is the exception — see below.)
166166

167167
| Callback | When it fires |
168168
|----------|---------------|
@@ -177,7 +177,7 @@ Callbacks register a `private` method to run at a specific point in the record's
177177
| `beforeUpdate` / `afterUpdate` | Around UPDATE only |
178178
| `beforeDelete` / `afterDelete` | Around DELETE |
179179

180-
Register callbacks in `config()``beforeSave("methodName")` or `beforeSave(methods="one,two")` when you want several. The methods themselves must be `private` and live on the same component.
180+
Register callbacks in `config()``beforeSave("methodName")` or `beforeSave(methods="one,two")` when you want several. The methods live on the same component and should be `private` — invocation is internal, so `public` also works, but a public callback method leaks onto the model's callable API.
181181

182182
```cfm {test:compile}
183183
component extends="Model" {
@@ -200,7 +200,24 @@ component extends="Model" {
200200

201201
The ordering is strict: `beforeValidation` → validation runs → `afterValidation``beforeSave` → (`beforeCreate` or `beforeUpdate`) → SQL write → (`afterCreate` or `afterUpdate`) → `afterSave`. If any `before*` callback returns `false`, the chain stops and the write is skipped.
202202

203-
`afterFind` is the one callback that fires without a write: it runs once per row returned by a finder, which makes it the right place for decorating loaded records with derived values.
203+
`afterFind` is the one callback that fires without a write: it runs once per record returned by a finder, which makes it the right place for decorating loaded records with derived values. Its contract differs by what the finder returns:
204+
205+
- **Object-returning finders** (`findOne`, `findByKey`, `findAll(returnAs="objects")`) — the callback receives the record's properties as named arguments. You can mutate `this` directly, or return a struct — the returned keys are applied back onto the object via `setProperties()`.
206+
- **Query-returning finders** (`findAll` by default) — there is no object per row. The callback receives the row's columns as named arguments and decorates by *returning a struct*; any keys you return are merged back into the row (new keys become new query columns). Mutating `this` does nothing here.
207+
208+
Both modes receive named arguments and both honor a returned struct, so the portable pattern — one callback that decorates objects and query rows alike — is to compute from `arguments` and return a struct:
209+
210+
```cfm {test:compile}
211+
component extends="Model" {
212+
function config() {
213+
afterFind("setFullName");
214+
}
215+
216+
private function setFullName() {
217+
return {fullName: arguments.firstName & " " & arguments.lastName};
218+
}
219+
}
220+
```
204221

205222
## Custom methods
206223

web/sites/guides/src/content/docs/v4-0-0/basics/validation-and-errors.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ component extends="Model" {
171171
- `unless` — inverse of `condition`; the rule runs only when the expression returns `false`.
172172

173173
<Aside type="tip">
174-
Validations on associations — like requiring child records to be valid before the parent saves — are covered in the associations guide (coming up in this series). `validatesAssociated` is the relevant helper.
174+
Validations on associations — like requiring child records to be valid before the parent saves — happen through nested properties: when a parent saves children via `nestedProperties()`, each child runs its own validations and an invalid child blocks the save. See the [associations guide](/v4-0-0/basics/associations/) for the setup. (There is no `validatesAssociated` helper in Wheels.)
175175
</Aside>
176176

177177
## Related guides

web/sites/guides/src/content/docs/v4-0-0/digging-deeper/authentication-patterns.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ local.di.map("sessionStrategy").to("wheels.auth.SessionStrategy").asSingleton();
6161

6262
Both are singletons — one instance per app lifetime. The authenticator holds its strategy registry in instance state, so a singleton is the correct scope.
6363

64-
Wire the session strategy into the authenticator on app init. A cold reload registers it once; the `hasStrategy` check keeps a second reload from stacking duplicates:
64+
Wire the session strategy into the authenticator on app init. The `hasStrategy` check just avoids needlessly re-registering on a warm reload — duplicates can't stack either way, because `registerStrategy()` replaces any existing entry with the same name:
6565

6666
```cfm {test:compile} title="config/app.cfm or equivalent init hook"
6767
if (StructKeyExists(application, "wheelsdi") && application.wheelsdi.containsInstance("authenticator")) {
@@ -340,7 +340,7 @@ component extends="wheels.Controller" {
340340
private function authenticate() {
341341
var result = service("authenticator").authenticate(request);
342342
if (!result.success) {
343-
if (request.format ?: "html" == "json") {
343+
if ((request.format ?: "html") == "json") {
344344
renderWith(data={error: result.error}, status=result.statusCode);
345345
} else {
346346
flashInsert(error="Please log in first");

0 commit comments

Comments
 (0)