What to build
lib/db.ts is a connection-pool wrapper exposing only query() / queryOne(). Every API route under app/api/ hand-rolls SQL, parameter coercion, and result-shape mapping. The shape of the data in Postgres is implicit, distributed across handlers, not encoded anywhere.
Apply the deletion test to db.ts as it stands: if it vanished, callers would import pg.Pool directly and write the same SQL — it's a pass-through. No leverage at the interface, no locality.
Introduce typed query modules per aggregate (e.g. lib/queries/submissions.ts, lib/queries/applications.ts) where each function takes typed args and returns typed rows. Routes become "JSON in, typed call, JSON out." lib/db.ts keeps the pool but stops being the import target for handlers.
This pairs with the Submission consolidation (#225) — if that lands first, lib/submission.ts can be the consumer of the submissions query module instead of routes.
Scope guard: this is not an ORM introduction. SQL stays raw. The deepening is "the queries each aggregate needs are visible in one place with typed signatures," not "abstract over the database."
Acceptance criteria
Blocked by
None - can start immediately. Interacts with #225 (Submission module) but not blocked by it.
What to build
lib/db.tsis a connection-pool wrapper exposing onlyquery()/queryOne(). Every API route underapp/api/hand-rolls SQL, parameter coercion, and result-shape mapping. The shape of the data in Postgres is implicit, distributed across handlers, not encoded anywhere.Apply the deletion test to
db.tsas it stands: if it vanished, callers would importpg.Pooldirectly and write the same SQL — it's a pass-through. No leverage at the interface, no locality.Introduce typed query modules per aggregate (e.g.
lib/queries/submissions.ts,lib/queries/applications.ts) where each function takes typed args and returns typed rows. Routes become "JSON in, typed call, JSON out."lib/db.tskeeps the pool but stops being the import target for handlers.This pairs with the Submission consolidation (#225) — if that lands first,
lib/submission.tscan be the consumer of the submissions query module instead of routes.Scope guard: this is not an ORM introduction. SQL stays raw. The deepening is "the queries each aggregate needs are visible in one place with typed signatures," not "abstract over the database."
Acceptance criteria
app/api/writes raw SQL directly — handlers call query-module functionslib/portfolio.ts/lib/work.ts/lib/submission.tsexpect — no ad-hoc shape mapping in handlersnpm run buildpasses/admin/submissionslists submissions, similarity preview returns results,/portfoliorendersBlocked by
None - can start immediately. Interacts with #225 (Submission module) but not blocked by it.