Commit 167418d
* fix(model): clear error when struct/array mass-assigned to non-association property (#2412)
When `model.new(params.user)` or `setProperties()` was called with a
struct or array value for a property that wasn't declared as a nested
association, `$setProperty` fell through to `this[property] = value`
and silently overwrote the model attribute with the bad shape. The
cast error then fired much later — inside a user callback like
`beforeValidation("normalizeEmail")` doing `LCase(this.email)` — and
Lucee reported "Can't cast Complex Object Type Struct to String"
pointing at the user's code instead of at the upstream form-data
mismatch.
The most common trigger is a curl POST whose body uses bracket-nested
keys without an `=` separator:
--data-urlencode "user[email][test@example.com]"
^ no `=` here ^
Lucee's form parser reads the trailing `[test@example.com]` as a
deeper nested-struct path, so `params.user.email` arrives as
`{"test@example.com": ""}` instead of a string. Browsers always
%-encode `@` and include the `=`, which is why the rendered form
works.
Fix: when `$setProperty` reaches the assignment branch and the value
is still a struct or array (i.e. no `hasOne`/`hasMany`/`belongsTo`
with `nestedProperties()` matched it), throw
`Wheels.PropertyIsIncorrectType` with a message naming the model and
property plus extendedInfo describing the likely root cause.
Legitimate nested-attribute assignments are unaffected because they
match the existing branches above the new guard.
Also corrects the chapter 6 tutorial gotcha note: the failure mode is
the missing `=` separator, not `@` encoding per se. The previous text
claimed `user[email]=alice@example.com` was enough to trigger the
bug, which contradicts the framework's existing
`createParamsSpec.cfc`.
Note: local `tools/test-local.sh` could not run from this worktree
because another worktree already held the wheels CLI's "wheels"
server-name lock. CI is authoritative — the new
`propertiesSpec.cfc` describe block exercises the four shapes
(struct + non-assoc, array + non-assoc, struct + nested-assoc happy
path, scalar happy path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(model): scope #2412 guard to real DB columns to avoid breaking hasMany/control-param paths
CI on the first push surfaced 21 regressions across crud, nested-
properties, association, and bulk-operations specs. Two patterns:
1. Arrays of model instances being assigned to `hasMany` properties
that don't have `nestedProperties()` enabled (e.g. `author.posts =
[postObj1, postObj2]` after `findOne(include="posts")`). The
original `$setProperty` skips its nested-collection branch when the
array contains objects (`!IsObject(arguments.value[1])` check), so
the array fell through to direct assignment — which is legitimate
for loaded associations.
2. Framework-internal control parameters like `useIndex` (a struct of
model→index-name mappings) leaking through `$setProperties` along
with finder/calculation arguments. These aren't model properties at
all, just args that happen to pass through the same path.
Both shapes hit the new guard's `IsStruct || IsArray` check and
threw, even though neither is the bug we're trying to catch.
Narrow the guard to fire only when the property is a real scalar
database column — i.e. `StructKeyExists(variables.wheels.class.properties,
arguments.property)`. `class.properties` is populated from schema
introspection at class init and contains only actual DB columns
(not associations, not transient args). The original bug case
(`email` on a User model) still hits the guard; everything else
falls through to the existing assignment branch unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(model): address Reviewer A/B consensus findings (round 1)
- Trim multi-line comment block in $setProperty to one short line (N1).
- Add regression specs for empty array and array-of-objects assigned
to a hasMany property with nestedProperties() enabled, confirming
the narrowed guard at properties.cfc:580 falls through for both
shapes (T1, A's findings for #2601).
- Fix tutorial curl example: the previous illustration used
`--data-urlencode "user[email][alice@example.com]"`, but curl reads
any unescaped `@` in such an argument as the start of a filename
(the `name@filename` form), so the example would error on disk
before ever reaching Lucee. Switch to a non-`@` bracket key and
call out the curl-level pitfall (B's missed-issue addendum).
C1 and C2 (the empty-array and array-of-objects regressions on
hasMany+nestedProperties) were already addressed by 6bae0e6, which
narrowed the guard to fire only when the property is a real scalar
DB column. The two new specs lock that behaviour against future
regressions.
CM1 (Signed-off-by trailer missing on b840c51 and 6bae0e6) cannot
be retroactively added by this bot stage — `.claude/commands/_shared-rails.md`
forbids any history-rewriting git subcommand. This commit is signed
off; the prior two will need a human-driven amend/rebase or a squash
merge to satisfy the DCO check.
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* fix(model): address Reviewer A/B consensus findings (round 2)
- vendor/wheels/tests/specs/model/propertiesSpec.cfc: trim two
multi-line comment blocks to single lines per CLAUDE.md
("never write multi-line comment blocks — one short line max").
The 8-line context block at the top of the new describe and the
3-line fixture explanation inside the "still accepts struct values
for nested associations" it() are both replaced with one-line
references; the issue #2412 background is already in the PR
description and commit messages.
- CHANGELOG.md: replace the curl example
`--data-urlencode "user[email][test@example.com]"` with
`--data-urlencode "user[email][badkey]"`. The `@` form would have
failed at the curl client (curl reads any unescaped `@` in
`--data-urlencode` as the start of a filename, the `name@filename`
form), so the example could not actually trigger the Lucee
parser behavior the entry is describing. Matches the tutorial
correction already applied in this PR.
Reviewer-flagged DCO trailers missing from b840c51 and 6bae0e6
remain a human-action item: rewriting published history is forbidden
by .claude/commands/_shared-rails.md, so the PR author needs to run
`git rebase --signoff develop && git push --force-with-lease` or
the PR can be merged via "Squash and merge" so the resulting commit
carries one DCO trailer.
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* fix(test): escape `#` in propertiesSpec it() descriptions (Lucee tag scanner crash)
CI from the most recent push returned HTTP 500 with "Invalid Syntax
Closing [#] not found" at propertiesSpec.cfc:437. The two new specs
added by 72effe4 and shipped through cc7ecaf embed the literal
PR reference `#2601` in their `it("... regression for #2601 review")`
description STRINGS — and CFML reads `#` inside a string literal as
the start of an `#expression#` interpolation. The scanner cannot find
the closing `#`, the whole CFC fails to compile, and the test runner
crashes for the entire model bundle (taking 800+ specs down).
CLAUDE.md ("Lucee Tag Scanner" memory + the "`#` escape gotcha" call-
out in the testing section) documents this exact trap. Comments
escape the scanner because `//` lines are not evaluated, which is why
the trimmed comment blocks in cc7ecaf still containing `#2412` were
fine — but the strings inside `it(...)` calls are not comments.
Escape both occurrences as `##2601` so the `#` survives as a literal.
Reproduced and verified locally against this worktree's Lucee 7 +
SQLite server: 810 model specs pass (3445 total core), 0 failures,
0 errors (the 12 remaining errors come from `BrowserLoginSpec` /
`BrowserRouteSpec` needing `wheels browser setup` and are unrelated
to this PR).
Signed-off-by: Peter Amiri <peter@alurium.com>
---------
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 1723dd5 commit 167418d
4 files changed
Lines changed: 58 additions & 1 deletion
File tree
- vendor/wheels
- model
- tests/specs/model
- web/sites/guides/src/content/docs/v4-0-0-snapshot/start-here/tutorial
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| 157 | + | |
157 | 158 | | |
158 | 159 | | |
159 | 160 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
577 | 577 | | |
578 | 578 | | |
579 | 579 | | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
580 | 591 | | |
581 | 592 | | |
582 | 593 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
402 | 402 | | |
403 | 403 | | |
404 | 404 | | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
405 | 450 | | |
406 | 451 | | |
407 | 452 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
499 | 499 | | |
500 | 500 | | |
501 | 501 | | |
502 | | - | |
| 502 | + | |
503 | 503 | | |
504 | 504 | | |
505 | 505 | | |
| |||
0 commit comments