refactor(backend): type the item query results (#159) #160

Merged
bermudalamb merged 2 commits from feature/159-type-query-results into main 2026-08-24 13:01:48 -05:00
Owner

First stage of typing the query results, and the one that sets the pattern. pg types rows as any[], so every row this application reads entered a strict codebase as any — 1 of roughly 184 query sites carried a type before this.

The row types live in itemSelect.ts, beside the selects that produce them, rather than in types.ts. They describe a projection rather than a table, and the two projections differ on purpose: ADMIN_ITEM_SELECT takes i.* while PUBLIC_ITEM_SELECT names its columns so the storefront never sees paypal_order_id or reserved_until. Typing both as "an items row" would quietly re-admit exactly the columns that select was written to exclude, so PublicItemRow and AdminItemRow share a base and the admin one adds the three fields it is allowed.

types.ts gains ItemTag, which the tags subquery has always built and nothing had named.

What this buys, demonstrated rather than claimed: introducing rows[0].price_cent at a read site now fails the build with "Property 'price_cent' does not exist on type 'ItemRowBase'. Did you mean 'price_cents'?". Before this it compiled, returned undefined, and reached the customer as an empty price.

What it does not buy is written into itemSelect.ts rather than left for the next reader to assume. pool.query<T> asserts a shape; it does not check the SQL, which TypeScript never reads. Dropping a column from a select without dropping it from its type compiles cleanly and every read goes on type-checking while being undefined at runtime. The selects and their types are kept in step by hand, and the integration suite is the only thing that catches them disagreeing, because it runs the real queries against a real schema. The acceptance criteria on #159 originally claimed the compiler would catch that; it will not, and the issue has been corrected.

Verified: tsc clean, and the four integration suites that exercise these selects pass 87/87.

Refs #159

First stage of typing the query results, and the one that sets the pattern. `pg` types `rows` as `any[]`, so every row this application reads entered a strict codebase as `any` — 1 of roughly 184 query sites carried a type before this. The row types live in itemSelect.ts, beside the selects that produce them, rather than in types.ts. They describe a projection rather than a table, and the two projections differ on purpose: ADMIN_ITEM_SELECT takes `i.*` while PUBLIC_ITEM_SELECT names its columns so the storefront never sees paypal_order_id or reserved_until. Typing both as "an items row" would quietly re-admit exactly the columns that select was written to exclude, so PublicItemRow and AdminItemRow share a base and the admin one adds the three fields it is allowed. types.ts gains ItemTag, which the tags subquery has always built and nothing had named. What this buys, demonstrated rather than claimed: introducing `rows[0].price_cent` at a read site now fails the build with "Property 'price_cent' does not exist on type 'ItemRowBase'. Did you mean 'price_cents'?". Before this it compiled, returned undefined, and reached the customer as an empty price. What it does not buy is written into itemSelect.ts rather than left for the next reader to assume. `pool.query<T>` asserts a shape; it does not check the SQL, which TypeScript never reads. Dropping a column from a select without dropping it from its type compiles cleanly and every read goes on type-checking while being undefined at runtime. The selects and their types are kept in step by hand, and the integration suite is the only thing that catches them disagreeing, because it runs the real queries against a real schema. The acceptance criteria on #159 originally claimed the compiler would catch that; it will not, and the issue has been corrected. Verified: tsc clean, and the four integration suites that exercise these selects pass 87/87. Refs #159
bermudalamb self-assigned this 2026-08-24 13:01:03 -05:00
bermudalamb added this to the Code Quality and Hardening 2 project 2026-08-24 13:01:03 -05:00
bermudalamb added 1 commit 2026-08-24 13:01:04 -05:00
refactor(backend): type the item query results (#159)
SonarQube Analysis / sonarqube (pull_request) Failing after 4m48s
Linting / lint (pull_request) Successful in 2m14s
15477b7d01
First stage of typing the query results, and the one that sets the pattern. `pg` types `rows` as `any[]`, so every row this application reads entered a strict codebase as `any` — 1 of roughly 184 query sites carried a type before this.

The row types live in itemSelect.ts, beside the selects that produce them, rather than in types.ts. They describe a projection rather than a table, and the two projections differ on purpose: ADMIN_ITEM_SELECT takes `i.*` while PUBLIC_ITEM_SELECT names its columns so the storefront never sees paypal_order_id or reserved_until. Typing both as "an items row" would quietly re-admit exactly the columns that select was written to exclude, so PublicItemRow and AdminItemRow share a base and the admin one adds the three fields it is allowed.

types.ts gains ItemTag, which the tags subquery has always built and nothing had named.

What this buys, demonstrated rather than claimed: introducing `rows[0].price_cent` at a read site now fails the build with "Property 'price_cent' does not exist on type 'ItemRowBase'. Did you mean 'price_cents'?". Before this it compiled, returned undefined, and reached the customer as an empty price.

What it does not buy is written into itemSelect.ts rather than left for the next reader to assume. `pool.query<T>` asserts a shape; it does not check the SQL, which TypeScript never reads. Dropping a column from a select without dropping it from its type compiles cleanly and every read goes on type-checking while being undefined at runtime. The selects and their types are kept in step by hand, and the integration suite is the only thing that catches them disagreeing, because it runs the real queries against a real schema. The acceptance criteria on #159 originally claimed the compiler would catch that; it will not, and the issue has been corrected.

Verified: tsc clean, and the four integration suites that exercise these selects pass 87/87.

Refs #159
bermudalamb added 1 commit 2026-08-24 13:01:38 -05:00
Merge branch 'main' into feature/159-type-query-results
SonarQube Analysis / sonarqube (pull_request) Failing after 4m39s
Linting / lint (pull_request) Successful in 1m56s
09e46d8565
bermudalamb merged commit d99cf28e18 into main 2026-08-24 13:01:48 -05:00
bermudalamb deleted branch feature/159-type-query-results 2026-08-24 13:01:48 -05:00
bermudalamb moved this to Review in Code Quality and Hardening 2 on 2026-08-24 13:15:16 -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#160