From 533da02fa08d9bc362b396431a3fa8ac128da4a4 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Wed, 19 Aug 2026 19:49:13 -0400 Subject: [PATCH 1/2] fix(templates): stop class fields shadowing Sequelize's attribute accessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- templates/api/express/models/user.ts | 21 ++++++++++++++++----- templates/api/fastify/models/user.ts | 21 ++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/templates/api/express/models/user.ts b/templates/api/express/models/user.ts index 425901d..c933488 100644 --- a/templates/api/express/models/user.ts +++ b/templates/api/express/models/user.ts @@ -9,12 +9,23 @@ export interface UserAttributes { updated_at?: Date; } +/* + * Attributes are `declare`, never `public x!: T`. + * + * Sequelize installs its attribute getters and setters on the prototype. A + * public class field 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 only surfaces at runtime as a query built with an undefined parameter. + * + * `declare` emits no field at all, so the accessors survive. + */ export class User extends Model implements UserAttributes { - public id!: string; - public email!: string | null; - public phone!: string | null; - public readonly created_at!: Date; - public readonly updated_at!: Date; + declare id: string; + declare email: string | null; + declare phone: string | null; + declare readonly created_at: Date; + declare readonly updated_at: Date; } const initializeUserModel = (sequelize: Sequelize) => { diff --git a/templates/api/fastify/models/user.ts b/templates/api/fastify/models/user.ts index 425901d..c933488 100644 --- a/templates/api/fastify/models/user.ts +++ b/templates/api/fastify/models/user.ts @@ -9,12 +9,23 @@ export interface UserAttributes { updated_at?: Date; } +/* + * Attributes are `declare`, never `public x!: T`. + * + * Sequelize installs its attribute getters and setters on the prototype. A + * public class field 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 only surfaces at runtime as a query built with an undefined parameter. + * + * `declare` emits no field at all, so the accessors survive. + */ export class User extends Model implements UserAttributes { - public id!: string; - public email!: string | null; - public phone!: string | null; - public readonly created_at!: Date; - public readonly updated_at!: Date; + declare id: string; + declare email: string | null; + declare phone: string | null; + declare readonly created_at: Date; + declare readonly updated_at: Date; } const initializeUserModel = (sequelize: Sequelize) => { From 7b278e531ef687948c950a07ed6873b2d285e19d Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Wed, 19 Aug 2026 21:14:37 -0400 Subject: [PATCH 2/2] fix(templates): stop class fields shadowing Sequelize's attribute accessors 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. --- .changeset/sequelize-declare-attributes.md | 20 ++++++++++++++++++++ templates/api/express/models/user.ts | 11 ----------- templates/api/fastify/models/user.ts | 11 ----------- 3 files changed, 20 insertions(+), 22 deletions(-) create mode 100644 .changeset/sequelize-declare-attributes.md diff --git a/.changeset/sequelize-declare-attributes.md b/.changeset/sequelize-declare-attributes.md new file mode 100644 index 0000000..d5de7f2 --- /dev/null +++ b/.changeset/sequelize-declare-attributes.md @@ -0,0 +1,20 @@ +--- +"seamless-templates": patch +--- + +fix(templates): declare Sequelize model attributes instead of using public class fields + +The `User` model in both API starters (`express` and `fastify`) declared its +attributes as `public id!: string`. Sequelize installs its attribute getters and +setters on the prototype, and a public class field 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. Sequelize warns about this at model +init. `declare` emits no field at all, so the accessors survive. + +Whether the field is emitted depends on `useDefineForClassFields`, which follows +`target`. Both starters compile at `target: ES2020`, where the field is erased +and the shadowing does not occur, so this is a guard rather than a repair of +behaviour anyone is seeing today. It matters because the guard is what keeps a +later `target` bump from silently breaking every model: at ES2022 the same code +returns undefined for every attribute, and the first symptom is a query built +with an undefined parameter on a handler that filters by `req.appUser.id`. diff --git a/templates/api/express/models/user.ts b/templates/api/express/models/user.ts index c933488..e4ec9e0 100644 --- a/templates/api/express/models/user.ts +++ b/templates/api/express/models/user.ts @@ -9,17 +9,6 @@ export interface UserAttributes { updated_at?: Date; } -/* - * Attributes are `declare`, never `public x!: T`. - * - * Sequelize installs its attribute getters and setters on the prototype. A - * public class field 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 only surfaces at runtime as a query built with an undefined parameter. - * - * `declare` emits no field at all, so the accessors survive. - */ export class User extends Model implements UserAttributes { declare id: string; declare email: string | null; diff --git a/templates/api/fastify/models/user.ts b/templates/api/fastify/models/user.ts index c933488..e4ec9e0 100644 --- a/templates/api/fastify/models/user.ts +++ b/templates/api/fastify/models/user.ts @@ -9,17 +9,6 @@ export interface UserAttributes { updated_at?: Date; } -/* - * Attributes are `declare`, never `public x!: T`. - * - * Sequelize installs its attribute getters and setters on the prototype. A - * public class field 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 only surfaces at runtime as a query built with an undefined parameter. - * - * `declare` emits no field at all, so the accessors survive. - */ export class User extends Model implements UserAttributes { declare id: string; declare email: string | null;