refactor(backend): type the customer query results (#159) #162

Merged
bermudalamb merged 1 commits from feature/159-type-customer-queries into main 2026-08-24 13:23:30 -05:00
Owner

The largest file, 43 query sites, now with none of its reads untyped. Typed sites across the backend go from 22 to 43.

CustomerRecord extends the existing CustomerRow rather than restating it, because that is the relationship that actually holds. CustomerRow was already there and is not a table row — it is the subset safe to return to the customer, written that way so adding a column could not silently start being echoed back by a ...c downstream. The full row read by SELECT * is that subset plus seven fields that are deliberately not on it, password_hash among them. Extending keeps the two connected: adding a column to the table means adding it to CustomerRecord and deciding at that moment whether it belongs in CustomerRow, which is exactly the decision the older comment is about.

The column list came from the live schema rather than from reading migrations, since the migrations are additive and reconstructing the current shape from six files invites getting a nullability wrong.

Typing the data export surfaced something worth a decision, and it is recorded in the code rather than quietly changed. GET /me/export runs SELECT * FROM orders and sends every column verbatim, including raw_event — the processor's entire capture payload. That is defensible for a GDPR export, since it is the customer's own transaction, but it is a decision rather than an accident, and it is now visible in a type instead of hidden behind any. The order-history route two functions above deliberately selects six named columns instead, which is the contrast that makes the export's behaviour worth confirming. No behaviour changed here; #159 is about types.

Nullability follows the schema rather than optimism: orders.amount_cents, status, item_id, customer_id and checkout_id are all nullable in Postgres, and customers.first_name and last_name are nullable despite registration requiring them, because customers who registered while the field was optional genuinely have none.

Verified: tsc clean, and the full integration suite passes 238/238 across 17 suites.

Refs #159

The largest file, 43 query sites, now with none of its reads untyped. Typed sites across the backend go from 22 to 43. CustomerRecord extends the existing CustomerRow rather than restating it, because that is the relationship that actually holds. CustomerRow was already there and is not a table row — it is the subset safe to return to the customer, written that way so adding a column could not silently start being echoed back by a `...c` downstream. The full row read by `SELECT *` is that subset plus seven fields that are deliberately not on it, password_hash among them. Extending keeps the two connected: adding a column to the table means adding it to CustomerRecord and deciding at that moment whether it belongs in CustomerRow, which is exactly the decision the older comment is about. The column list came from the live schema rather than from reading migrations, since the migrations are additive and reconstructing the current shape from six files invites getting a nullability wrong. Typing the data export surfaced something worth a decision, and it is recorded in the code rather than quietly changed. `GET /me/export` runs `SELECT * FROM orders` and sends every column verbatim, including raw_event — the processor's entire capture payload. That is defensible for a GDPR export, since it is the customer's own transaction, but it is a decision rather than an accident, and it is now visible in a type instead of hidden behind `any`. The order-history route two functions above deliberately selects six named columns instead, which is the contrast that makes the export's behaviour worth confirming. No behaviour changed here; #159 is about types. Nullability follows the schema rather than optimism: orders.amount_cents, status, item_id, customer_id and checkout_id are all nullable in Postgres, and customers.first_name and last_name are nullable despite registration requiring them, because customers who registered while the field was optional genuinely have none. Verified: tsc clean, and the full integration suite passes 238/238 across 17 suites. Refs #159
bermudalamb self-assigned this 2026-08-24 13:23:02 -05:00
bermudalamb added this to the Code Quality and Hardening 2 project 2026-08-24 13:23:02 -05:00
bermudalamb added 1 commit 2026-08-24 13:23:03 -05:00
refactor(backend): type the customer query results (#159)
SonarQube Analysis / sonarqube (pull_request) Failing after 5m3s
Linting / lint (pull_request) Successful in 1m57s
c5fe84fba5
The largest file, 43 query sites, now with none of its reads untyped. Typed sites across the backend go from 22 to 43.

CustomerRecord extends the existing CustomerRow rather than restating it, because that is the relationship that actually holds. CustomerRow was already there and is not a table row — it is the subset safe to return to the customer, written that way so adding a column could not silently start being echoed back by a `...c` downstream. The full row read by `SELECT *` is that subset plus seven fields that are deliberately not on it, password_hash among them. Extending keeps the two connected: adding a column to the table means adding it to CustomerRecord and deciding at that moment whether it belongs in CustomerRow, which is exactly the decision the older comment is about.

The column list came from the live schema rather than from reading migrations, since the migrations are additive and reconstructing the current shape from six files invites getting a nullability wrong.

Typing the data export surfaced something worth a decision, and it is recorded in the code rather than quietly changed. `GET /me/export` runs `SELECT * FROM orders` and sends every column verbatim, including raw_event — the processor's entire capture payload. That is defensible for a GDPR export, since it is the customer's own transaction, but it is a decision rather than an accident, and it is now visible in a type instead of hidden behind `any`. The order-history route two functions above deliberately selects six named columns instead, which is the contrast that makes the export's behaviour worth confirming. No behaviour changed here; #159 is about types.

Nullability follows the schema rather than optimism: orders.amount_cents, status, item_id, customer_id and checkout_id are all nullable in Postgres, and customers.first_name and last_name are nullable despite registration requiring them, because customers who registered while the field was optional genuinely have none.

Verified: tsc clean, and the full integration suite passes 238/238 across 17 suites.

Refs #159
bermudalamb merged commit 36c9fe9227 into main 2026-08-24 13:23:30 -05:00
bermudalamb deleted branch feature/159-type-customer-queries 2026-08-24 13:23:31 -05:00
bermudalamb moved this to Review in Code Quality and Hardening 2 on 2026-08-24 15:31:39 -05:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#162