Skip to content

fix(schema): cap custState at 2 chars to match DB column#265

Merged
CryptoJones merged 1 commit into
masterfrom
fix/customer-schema-custstate-2char-limit
May 19, 2026
Merged

fix(schema): cap custState at 2 chars to match DB column#265
CryptoJones merged 1 commit into
masterfrom
fix/customer-schema-custstate-2char-limit

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Summary

customer.schema.js validated custState as z.string().max(255), but the DB column is character varying(2) (see setup/TimeTracker.sql). Any 3+ char value passed zod and then surfaced as a 500 "Error!" at the postgres INSERT — "value too long for type character varying(2)".

What changed

  • Tightened the zod constraint to .length(2) so the validator catches the mismatch at the request boundary.
  • Added an inline comment explaining the link to the DB column type and why exactly-2 is correct (US state codes + Canadian province codes both fit).

Impact

  • Callers POSTing valid 2-char codes ("NE", "CA", etc.): unchanged.
  • Callers previously POSTing >2 chars: now get a clean 400 with a field-level message, instead of an opaque 500.

Test plan

  • npm run lint && npm test — 742 passing
  • Existing customer-create tests continue to pass; the contract for valid states is unchanged.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

`customer.schema.js` validated `custState` as `z.string().max(255)`,
but `setup/TimeTracker.sql` (and the Sequelize column on every
deployed schema) defines the column as `character varying(2)`. The
mismatch meant any 3+ char value passed the middleware layer cleanly
and then surfaced as a 500 "Error!" at the postgres INSERT —
"value too long for type character varying(2)".

Tighten the zod schema to `.length(2)` so the validation message
identifies the actual problem (`custState must be exactly 2 chars`)
at the request boundary instead of opaque server-error noise from
the persistence layer. US state codes (NE, CA, etc.) and Canadian
province codes (AB, BC, etc.) all fit the 2-char shape; the DB
column was sized this way from day one.

No callers POSTing valid 2-char codes are affected. Callers that
were previously broken at INSERT now get a clearer 400 instead of
a 500 — strictly a UX improvement.

742 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CryptoJones CryptoJones merged commit 78abcf5 into master May 19, 2026
3 checks passed
@CryptoJones CryptoJones deleted the fix/customer-schema-custstate-2char-limit branch May 19, 2026 13:45
CryptoJones added a commit that referenced this pull request May 19, 2026
…ors (#270)

The OpenAPI \`Customer\` component declared every string field as a
bare \`type: 'string'\` with no length constraint. The actual zod
validators in customer.schema.js have concrete max lengths (and a
fixed \`.length(2)\` on custState per #265). SDK generators
(openapi-typescript et al.) consume the OpenAPI spec, so without
these constraints client-side types miss the real bounds and a
SDK-driven 400 surprises the caller.

What changed:
  - custState: type 'string' with min/maxLength: 2 (per #265 fix)
  - custZip / custPhone / custEmail: explicit maxLength matching
    the zod schema (32 / 64 / 255)
  - custCompanyName / FName / LName / Address1/2 / City: maxLength
    255 each

No behavior change — runtime still validates via the same zod
schema; this just brings the published contract into agreement
with reality. 760 tests still pass.

Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CryptoJones added a commit that referenced this pull request May 19, 2026
… DB NOT NULL (#277)

setup/TimeTracker.sql defines all three columns as text NOT NULL, but
customer.schema.js marked them \`.optional()\`. Same class of drift as
#265's custState fix: callers omitting any of these fields passed
zod and surfaced as a 500 at the postgres INSERT layer ("null value
in column 'custFName' violates not-null constraint") instead of a
clean 400 with a field-level message.

Removed \`.optional()\` from the three required fields. The whitelist
message and the surrounding doc comment are updated to call out the
DB-NOT-NULL contract explicitly so future readers don't undo it.

Test impact: four api-tests (customer-create / customer-bulk /
validation / idempotency) sent partial bodies expecting the
controller's authKey-missing 403 to short-circuit. With required
fields now enforced by zod earlier in the chain, those tests now
hit a 400 before reaching auth. Each test was updated to send a
complete required-field body — preserving the original intent
(testing auth-fail behavior) while accommodating the new
validation contract.

760 tests still pass (was 760, no count change — same paths just
with non-partial bodies).

Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CryptoJones added a commit that referenced this pull request May 19, 2026
… NULL (#279)

Same drift class as #265 (custState) and #277 (custCompanyName et al.),
but with a twist: the single-create controller already filled in the
missing-invPaid case with \`if (payload.invPaid === undefined)
payload.invPaid = false;\`, so the bug only manifested in the bulk
path, which routes through \`makeBulkCreateIndirect\` and doesn't
have entity-specific defaults.

A bulk POST like
  { invoices: [{ invCustId: 5, invDate: "2026-01-01",
                 invDueDate: "2026-02-01" }] }
passed the schema (invPaid was \`.optional()\`), and then tripped
"null value in column invPaid violates not-null constraint" at the
postgres INSERT — surfacing as a 500 instead of a clean accepted
row.

Switching the schema to \`z.boolean().default(false)\` fills the
value at the validator boundary, so both paths now get
\`invPaid: false\` for free. The single-create controller's
explicit default becomes dead defense but stays harmless.

760 tests still pass.

Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CryptoJones added a commit that referenced this pull request May 19, 2026
…#285)

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: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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