Skip to content

fix(templates): stop class fields shadowing Sequelize's attribute accessors - #48

Merged
Bccorb merged 2 commits into
mainfrom
fix/sequelize-class-fields
Aug 20, 2026
Merged

fix(templates): stop class fields shadowing Sequelize's attribute accessors#48
Bccorb merged 2 commits into
mainfrom
fix/sequelize-class-fields

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Sequelize installs its attribute getters and setters on the prototype. public id!: string is emitted as an own property initialised to undefined, which shadows them: user.id reads undefined while user.get("id") returns the row's value. declare emits no field at all, so the accessors survive.

Both API starters are changed, express and fastify. The diff is five lines per model and nothing else.

This is a guard, not a repair

Whether the field is emitted depends on useDefineForClassFields, which follows target. Both starters compile at target: ES2020 in tsconfig.json, and tsconfig.build.json only extends it, so dev (tsx) and prod (tsc) agree. Below ES2022 a field with a definite assignment assertion and no initialiser is erased entirely, so nothing is shadowed today.

Confirmed by building this branch:

$ npm run build && sed -n '/class User extends Model/,/^}/p' dist/models/user.js
export class User extends Model {
}

And confirmed in both directions with a live model, no database needed:

target ES2020:  public id!:  .id = "abc-123"   .get('id') = "abc-123"
target ES2022:  public id!:  .id = undefined   .get('id') = "abc-123"
                declare id:  .id = "abc-123"   .get('id') = "abc-123"
(sequelize) Warning: Model "Old" is declaring public class fields for attribute(s): "id", "email".

So this does not fix behaviour anyone is seeing right now. It matters because it disarms a target bump: at ES2022 every attribute on every model returns undefined, and the first symptom is a query built with an undefined parameter on any handler that filters by req.appUser.id, which requireUser assigns from a Sequelize instance. Moving to a Node 24 baseline is an ordinary thing to do and would trigger it silently.

If you have actually observed WHERE parameter "user_id" has invalid "undefined" value, this branch will not make it go away and the cause is elsewhere.

Follow-up worth considering

Nothing in the repo now states the rule, so the pattern is carried by example only. The generation pipeline reads api/models/user.ts as its worked model example on every run, and reference.ts has a "Conventions to match" block where models are already described. A one line rule there ("attributes are always declare, never public x!: T") would make new generated models safe by rule rather than by imitation.

Checks

npm run validate passes. npm run check (typecheck, lint, format:check, test) and npm run build pass in both API starters.

Note for review

This branch carries two commits because git commit --amend was unavailable when the first fixup was prepared. The earlier commit's message contains an em dash, which the working standards forbid. Squash on merge and both go away.

Bccorb and others added 2 commits August 19, 2026 21:05
…essors

Sequelize installs attribute getters and setters on the prototype. `public id!:
string` is emitted as an own property initialised to undefined, which shadows
them: `user.id` reads undefined while `user.get("id")` returns the row's value.

Nothing catches it. The types say `string`, so it compiles, and it surfaces only
at runtime as a query built with an undefined parameter — in practice as
`WHERE parameter "user_id" has invalid "undefined" value` the first time a
signed-in person asks for their own records. Sequelize warns about it at model
init, into a log nobody reads during a scaffold.

`declare` emits no field, so the accessors survive. The caveat is written next to
the model, since this is a pattern anyone adding a model will otherwise copy.
…essors

Sequelize installs attribute getters and setters on the prototype. `public id!:
string` is emitted as an own property initialised to undefined, which shadows
them: `user.id` reads undefined while `user.get("id")` returns the row's value.

Nothing catches it. The types say `string`, so it compiles, and it surfaces only
at runtime as a query built with an undefined parameter, in practice as
`WHERE parameter "user_id" has invalid "undefined" value` the first time a
signed-in person asks for their own records. Sequelize warns about it at model
init, into a log nobody reads during a scaffold.

Whether the field is emitted at all depends on `useDefineForClassFields`, which
follows `target`. Both starters compile at ES2020, where the field is erased and
nothing is shadowed, so this guards the pattern rather than repairing behaviour
anyone is seeing today. The guard is the point: at ES2022 the same code returns
undefined for every attribute, and a `target` bump is an ordinary thing to do.

`declare` emits no field, so the accessors survive.
@Bccorb
Bccorb force-pushed the fix/sequelize-class-fields branch from 65b720a to 7b278e5 Compare August 20, 2026 02:01
@Bccorb
Bccorb merged commit c9b5f59 into main Aug 20, 2026
6 checks passed
@Bccorb
Bccorb deleted the fix/sequelize-class-fields branch August 20, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant