From 12e69b466986bb1333696229208a3cb88ebdd5bd Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 10:41:05 -0500 Subject: [PATCH] chore(model): align Customer column types and nullability with the DB The Customer model was the outlier in the models/ directory: every string field declared as Sequelize.STRING (varchar 255 default) with no allowNull, even though setup/TimeTracker.sql has the corresponding columns as text NOT NULL (or text NULL, varchar(2), varchar(32)) for specific fields. Every other model (Worker, BillingType, InventoryItem, Job, Invoice, ...) already used TEXT / STRING(N) accurately with explicit allowNull declarations. Aligned each field with the DDL: - custCompanyName / custFName / custLName: TEXT, allowNull: false - custAddress1 / 2 / custCity / custZip / custEmail: TEXT - custState: STRING(2) - custPhone: STRING(32) - custArch: BOOLEAN, allowNull: false, defaultValue: false - custCompId: INTEGER, allowNull: false No behavior change for typical callers: - Zod validation (#265, #277) already enforces the NOT NULL fields + custState length at the request boundary, so this is purely defense in depth. - Removing the STRING-255 implicit cap relaxes the model to match the actual DB TEXT (unbounded), but zod's max(255) still caps inputs at the API layer. 760 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/models/customer.model.js | 88 +++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/app/models/customer.model.js b/app/models/customer.model.js index fd351bd..0ed5bb2 100644 --- a/app/models/customer.model.js +++ b/app/models/customer.model.js @@ -1,69 +1,73 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Aaron K. Clark "use strict"; + +/** + * Customer — a person/organization a TimeEntry attaches to. + * + * Column types + nullability mirror setup/TimeTracker.sql (the + * authoritative DDL). Earlier this model declared every string field + * as `Sequelize.STRING` (varchar 255) with no `allowNull` — + * misrepresenting the underlying `text NOT NULL` columns. Aligning + * here means sequelize-level validation matches the DB constraint + * a row would actually fail on, and any downstream consumer reading + * the model definition (e.g. SDK code-gen, migration diffs) sees the + * real shape. + * + * Soft-deletes via custArch — defaultScope filters `false` so reads + * never see archived rows without `.unscoped()` (which this codebase + * never uses; cf. tests/unit/default-scope.test.js). + */ module.exports = (sequelize, Sequelize) => { const Customer = sequelize.define('Customer', { custId: { field: 'custId', type: Sequelize.INTEGER, autoIncrement: true, - primaryKey: true + primaryKey: true, }, + // text NOT NULL in the DB — see setup/TimeTracker.sql. custCompanyName: { field: 'custCompanyName', - type: Sequelize.STRING + type: Sequelize.TEXT, + allowNull: false, }, custFName: { field: 'custFName', - type: Sequelize.STRING + type: Sequelize.TEXT, + allowNull: false, }, custLName: { field: 'custLName', - type: Sequelize.STRING - }, - custAddress1: { - field: 'custAddress1', - type: Sequelize.STRING - }, - custAddress2: { - field: 'custAddress2', - type: Sequelize.STRING - }, - custCity: { - field: 'custCity', - type: Sequelize.STRING - }, - custState: { - field: 'custState', - type: Sequelize.STRING - }, - custZip: { - field: 'custZip', - type: Sequelize.STRING + type: Sequelize.TEXT, + allowNull: false, }, + // text NULL — nullable address fields. + custAddress1: { field: 'custAddress1', type: Sequelize.TEXT }, + custAddress2: { field: 'custAddress2', type: Sequelize.TEXT }, + custCity: { field: 'custCity', type: Sequelize.TEXT }, + // varchar(2) — US state codes / Canadian province codes. + custState: { field: 'custState', type: Sequelize.STRING(2) }, + custZip: { field: 'custZip', type: Sequelize.TEXT }, custArch: { field: 'custArch', type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, }, - custPhone: { - field: 'custPhone', - type: Sequelize.STRING - }, - custEmail: { - field: 'custEmail', - type: Sequelize.STRING - }, + // varchar(32) — phone numbers stored as a string, not parsed. + custPhone: { field: 'custPhone', type: Sequelize.STRING(32) }, + custEmail: { field: 'custEmail', type: Sequelize.TEXT }, custCompId: { field: 'custCompId', - type: Sequelize.INTEGER - } - }, - { - tableName: 'Customer', - timestamps: true, - defaultScope: { where: { custArch: false } } - } - ); + type: Sequelize.INTEGER, + allowNull: false, + }, + }, { + tableName: 'Customer', + timestamps: true, + defaultScope: { where: { custArch: false } }, + }); return Customer; -} +};