Devafusion.net

Engineering log

Database tooling: Drizzle ORM behind a repository pattern

Set up Drizzle ORM, drizzle-kit migrations, drizzle-zod validation, and spec-only OpenAPI generation, all proven against a local in-memory PGlite Postgres rather than a real network database. No production data was touched - the engineering log stays on static files for now; this slice proves the tooling and the SOLID repository pattern that will carry it, and any future OAuth/contact-form data, into PostgreSQL.

Decisions

  • Put a LogEntryRepository interface between every consumer and Drizzle - DrizzleLogEntryRepository is the only file that imports drizzle-orm directly, so a future ORM swap only means writing a new class, not touching every call site (SOLID's Dependency Inversion Principle).
  • Chose Drizzle over Prisma: a plain TypeScript schema rather than a second DSL to keep in sync by hand (DRY), no Edge-runtime compatibility baggage to reason about (irrelevant anyway on this project's Node-standalone Linux App Service hosting), and a mature, stable generate/migrate workflow versus Prisma's own newest migration engine, which Prisma itself describes as still early.
  • Made the repository's db client a constructor parameter defaulting to the shared app client, instead of a hard module-level import - this is what actually lets a Vitest suite substitute an in-memory PGlite client with no real network connection or DATABASE_URL needed.
  • Rejected Neon for local/CI Postgres, even for throwaway schema testing - it would add a dependency on a second, unrelated third-party vendor for zero benefit over PGlite/Docker, and production data must stay in the Azure-hosted instance ADR-0010 already committed to. PGlite (an in-process WASM Postgres) covers local dev and Vitest; a real Docker postgres:16 container was considered for CI but not added this slice since nothing yet needs production-parity behaviour a WASM build can't already provide - reconsidering that call is explicitly this project's cue against YAGNI, not a permanent decision.
  • Discovered ES module import hoisting breaks the naive 'call extendZodWithOpenApi before creating schemas in the same file' pattern - imports resolve before any top-level statement runs, regardless of source order, so the patch had to move into its own dedicated side-effect module imported by the schema file itself.
  • Ran npm audit as part of installing new dependencies and found two critical, pre-existing CVEs in next@16.3.2 (unauthenticated RCE on Windows-hosted servers, and in the Image Optimization API) that predated this session entirely - upgraded to next@16.3.5 as a deliberate, logged Deprecation Upgrade rather than leaving a known-vulnerable version in place because it was inconvenient mid-slice.
  • Accepted one remaining moderate-severity esbuild advisory, transitively required by drizzle-kit's own dependency tree - it's dev-tooling-only exposure (the vulnerable dev server only runs when a developer manually invokes a drizzle-kit command locally) with no fix available except a major-version downgrade of drizzle-kit itself, which would be a worse regression than the risk it fixes.
  • Chose the backend-first schema-sharing strategy (drizzle-zod's createInsertSchema/createSelectSchema deriving Zod from the Drizzle table) over a hand-authored-Zod-first or permanently-separate-client-schema split - the DB table stays the single source of truth for nullability/defaults/enum constraints, which is exactly what a future registration/feedback form (with reCAPTCHA and MFA already on the roadmap) needs from its client-side validation schema. Added the server-only package and imported it at the top of db/schema.ts and log-entries.zod.ts so an accidental client-side import of either module fails the build immediately instead of silently bundling drizzle-orm/pg into browser JavaScript.

Milestones

  • Installed drizzle-orm, pg, zod, drizzle-zod (dependencies) and drizzle-kit, @electric-sql/pglite, @types/pg, tsx (devDependencies), plus @asteasolutions/zod-to-openapi.
  • Added db/schema.ts (the log_entries Drizzle table), db/client.ts (the pooled node-postgres client), and drizzle.config.ts.
  • Added features/log/repository.ts (the LogEntryRepository interface) and features/log/drizzle-log-entry-repository.ts (the Drizzle adapter).
  • Added features/log/schema/log-entries.zod.ts (drizzle-zod generated select/insert schemas) and features/log/schema/zod-openapi-setup.ts (the extendZodWithOpenApi side-effect module).
  • Added scripts/generate-openapi.ts, wired to a new npm run db:openapi script, producing a spec-only openapi-docs.json build artifact (gitignored).
  • Added features/log/__tests__/drizzle-log-entry-repository.test.ts, gated by TEST_DB_ACTIONS, running all assertions against a real in-memory PGlite Postgres.
  • Upgraded next to 16.3.5 (and eslint-config-next to match) to close two critical CVEs found via npm audit; ran npm audit fix for an unrelated js-yaml advisory.
  • Added docs/adr/0011-drizzle-orm-and-repository-pattern.md.
  • Installed server-only and added it as an explicit import to db/schema.ts and features/log/schema/log-entries.zod.ts.
  • Added a Database, Schema Sharing & Future Forms section to src/web/AGENTS.md codifying the server-only guard rule and the Server-Action/backend-first-validation pattern for the next real form.
  • Updated infrastructure/AGENTS.md's PostgreSQL section, which had gone stale (still said no instance existed) - now describes the live instance, the drizzle-kit generate/migrate workflow as the only sanctioned schema-change path, and the deliberately deferred CD migration step.

Validation

  • npm run typecheck and npm run lint both passed clean
  • npm run build succeeded with every route still static/SSG - the new db/repository code is not yet imported by any page, and Pool's lazy connection behaviour confirmed it doesn't throw at module load with DATABASE_URL unset
  • npm run test:unit passed all 15 existing tests plus the 3 new PGlite-backed repository tests when TEST_DB_ACTIONS=true, and confirmed all 3 are correctly skipped when the flag is unset
  • npm run db:openapi generated a correct openapi-docs.json distinguishing LogEntry (select, all fields required) from LogEntryInsert (only the fields with no default/nullable constraint required)
  • npm audit reported 0 vulnerabilities after the next upgrade and js-yaml fix, with one remaining moderate esbuild advisory explicitly accepted and documented rather than silently ignored

This site uses Google Analytics to understand traffic. See the privacy and cookies page for details.