feat(F-002): database foundation with migrations and dev compose

- node-pg-migrate + pg: baseline migration (extensions, app_meta) with working down
- src/infrastructure/db fail-fast pool and typed query helper
- docker-compose: postgres:16-alpine + redis:7-alpine with one-command up
- table naming convention <module>_<table> documented in README
- integration tests (6) against real PostgreSQL; strict identifier validation
  for test DDL after security-gate hardening round
- deps justified in spec/tech.md; all gates approved; verify.sh green
This commit is contained in:
rikrdo
2026-08-14 22:00:16 +02:00
parent 1d4eebca54
commit 425fedd13e
30 changed files with 1197 additions and 14 deletions

View File

@@ -0,0 +1,26 @@
# Architect — F-002 Database foundation with module-owned schemas
done -> work/artifacts/F-002/architect.md
## Decision summary
- Migration tool: node-pg-migrate + pg driver. Pure npm deps, boring, up/down support, version table owned by the tool.
- Single PostgreSQL instance, module ownership by table prefix (`<module>_<table>`). No per-module PG schemas — simplicity first; prefix rule documented and exemplified.
- Baseline migration only ships foundation objects (extensions + `app_meta`). Business tables arrive with their modules.
- Dev environment: docker-compose with postgres:16-alpine + redis:7-alpine, one command up.
- Env via Node 22 `--env-file`; fail fast when `DATABASE_URL` missing. Integration tests skip explicitly when `TEST_DATABASE_URL` absent — no silent magic.
## Expected blast radius
```text
EXPECTED BLAST RADIUS
Modules modified: src/infrastructure/db (new)
Modules indirectly affected: none (no HTTP API, no business modules)
Database changes: baseline migration (extensions, app_meta) - greenfield
API changes: none
Events added/changed: none
Risk level: low
```
## Handoff to implementer
Follow specs/F-002-database-foundation/TASKS.md. Docker daemon is available (verified at intake). Definition of done for build stage: compose up green, migrations up/down/up demonstrated against real PostgreSQL, integration tests passing with TEST_DATABASE_URL, and evidence in implementer.md.

View File

@@ -0,0 +1,20 @@
# Documenter — F-002 Database foundation with module-owned schemas
done -> work/artifacts/F-002/documenter.md
## Why documentation applies
F-002 adds developer-facing contracts: database commands, migration workflow, and the table naming convention.
## Documents reviewed/produced
- `project/README.md` (updated at build stage, verified accurate):
- Database section lists every new script: docker:up, docker:down, db:up, db:down, db:status, test:integration — all present in package.json (cross-checked programmatically)
- `.env.example` copy step documented
- Table naming convention section: `<module>_<table>` rule, no cross-prefix queries, migrations immutable
- `spec/tech.md`: dependency justification table extended with pg, node-pg-migrate, @types/pg (security policy requirement)
- `specs/F-002-database-foundation/` SPEC/DESIGN/TASKS/TESTS consistent with implementation (migration filename corrected to .js at design stage)
## Verification
- package.json script keys vs README commands cross-checked: all db:/docker: commands documented; remaining scripts (build, start, test, typecheck, lint, lint:boundaries) already documented in F-001 section.
## Pending docs (not this ticket)
- Per-module data access contracts documented as each module lands (F-005+).

View File

@@ -0,0 +1,63 @@
# Implementer — F-002 Database foundation with module-owned schemas
done -> work/artifacts/F-002/implementer.md
## Files created (all under project/ unless noted)
- docker-compose.yml (postgres:16-alpine + redis:7-alpine, named volumes, healthchecks, dev-only creds)
- .env.example
- migrations/001_baseline.js (node-pg-migrate; citext + pgcrypto extensions, app_meta table, with down)
- src/infrastructure/db/pool.ts (createPoolFromEnv fail-fast, typed query helper)
- src/infrastructure/db/tests/db-test-support.ts (recreateDatabase, runMigrations, tableExists helpers; strict identifier validation for DDL-interpolated db name)
- src/infrastructure/db/tests/db-test-support.test.ts (pure helper tests incl. identifier rejection)
- src/infrastructure/db/tests/migrations.itest.ts
- src/infrastructure/db/tests/pool.itest.ts
- scripts/db-status.mjs
## Files modified
- package.json: deps (pg, node-pg-migrate; @types/pg dev) + scripts (test:integration, docker:up/down, db:up/down/status)
- vitest.config.ts: include *.itest.ts; fileParallelism false (integration files share one test DB)
- .gitignore: .env
- README.md: Database section + table naming convention
- spec/tech.md: dependency justification rows for pg / node-pg-migrate / @types/pg
- specs/F-002-database-foundation/DESIGN.md + TASKS.md: migration filename corrected to .js (node-pg-migrate native format)
## Database migrations
- 001_baseline: extensions citext + pgcrypto, table app_meta(key, value, updated_at). Down fully reverts.
- Tracking table: pgmigrations (owned by node-pg-migrate).
## API changes
- None.
## Tests added
- migrations.itest.ts: fresh up creates schema / second up is no-op / down rolls back cleanly
- pool.itest.ts: SELECT 1 roundtrip / app_meta insert-read-delete via query helper / fail fast without DATABASE_URL
## Tests passed (evidence)
```
npm run docker:up -> mdv-dev-postgres Healthy, mdv-dev-redis Healthy
npm run db:up (1st) -> MIGRATION 001_baseline (UP), Migrations complete!
npm run db:up (2nd) -> No migrations to run! (no-op)
npm run db:status -> 001_baseline listed as applied
npm run db:down -> MIGRATION 001_baseline (DOWN); psql to_regclass('public.app_meta') -> empty
npm run db:up (again) -> re-applies cleanly
npm run lint -> OK
npm run lint:boundaries -> Boundary check OK: 11 file(s) checked
npm run typecheck -> exit 0
npm run build -> exit 0
npm test -> 4 files passed, 2 integration files skipped explicitly without TEST_DATABASE_URL (11 passed | 6 skipped)
npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6) against real PostgreSQL 16
docker exec mdv-dev-redis redis-cli ping -> PONG
```
## Known limitations
- node-pg-migrate programmatic runner requires explicit migrationsTable (passed 'pgmigrations' in the test helper to match CLI default).
- Integration tests drop/create the shared test database; vitest fileParallelism disabled to avoid the race (documented in vitest.config.ts).
- No connection retry/backoff yet; not needed while only dev scripts connect.
## Follow-up work
- F-004 will centralize env/config (current fail-fast reader stays until then).
- First real module tables arrive with F-005+ following <module>_<table> convention.
## Security hardening round (requested by security gate)
- db-test-support.ts now validates the database name from TEST_DATABASE_URL against /^[a-zA-Z_][a-zA-Z0-9_]*$/ before interpolating it into DROP/CREATE DATABASE DDL; rejection covered by db-test-support.test.ts.
- Re-verified: lint, typecheck, npm test (11 passed), test:integration (6 passed).

View File

@@ -0,0 +1,34 @@
{
"feature_id": "F-002",
"agent": "leader",
"verdict": "APPROVED",
"summary": "F-002 closed. Database foundation implemented and proven against real PostgreSQL 16 + Redis 7: migrations up/down/no-op, fail-fast pool, one-command dev environment, naming convention documented. All gates APPROVED, verify.sh exit 0.",
"gates": {
"reviewer": "APPROVED (reviewer.json)",
"security": "APPROVED (security.json, after identifier-validation hardening round)",
"qa": "APPROVED (qa.json)",
"verify_sh": "exit 0"
},
"deliverables": [
"project/migrations/001_baseline.js with working down migration",
"project/src/infrastructure/db/pool.ts (fail-fast pool + typed query helper)",
"project/docker-compose.yml (postgres:16-alpine + redis:7-alpine)",
"npm scripts: db:up/down/status, docker:up/down, test:integration",
"6 integration tests passing against real PostgreSQL; helper identifier validation regression-tested",
"specs/F-002-database-foundation complete; spec/tech.md dependency justifications"
],
"process_notes": [
"Security gate bounced one low finding (DDL identifier interpolation in test support) back to build; fixed with strict validation + tests, then re-approved. The gate worked as designed."
],
"next_feature_hint": "F-003 (HTTP foundation/request context) and F-004 (config/flags) only depend on F-001; F-005 identity now unblocked by F-002",
"evidence": [
"work/artifacts/F-002/architect.md",
"work/artifacts/F-002/implementer.md",
"work/artifacts/F-002/reviewer.json",
"work/artifacts/F-002/security.json",
"work/artifacts/F-002/qa.json",
"work/artifacts/F-002/documenter.md",
"./scripts/verify.sh exit 0 at close"
],
"timestamp": "2026-08-14T20:04:00Z"
}

View File

@@ -0,0 +1,42 @@
{
"feature_id": "F-002",
"agent": "qa",
"verdict": "APPROVED",
"summary": "All 5 acceptance criteria verified with fresh executions against real PostgreSQL 16 + Redis 7 in docker.",
"traceability": [
{
"criterion": "AC1: fresh database + migrations up creates schema; rerun is no-op",
"test": "migrations.itest.ts (recreates DB fresh each run) + CLI: npm run db:up on applied DB",
"result": "PASS (fresh up -> app_meta exists; second up -> 'No migrations to run!')"
},
{
"criterion": "AC2: down rolls back cleanly",
"test": "migrations.itest.ts down case + reviewer CLI roundtrip (db:down verified app_meta gone, db:up re-applied)",
"result": "PASS"
},
{
"criterion": "AC3: table naming convention documented and enforced",
"test": "README.md 'Table naming convention' section + baseline exemplar app_meta + immutability rule",
"result": "PASS"
},
{
"criterion": "AC4: dev PostgreSQL and Redis start with one command",
"test": "full docker:down then npm run docker:up",
"result": "PASS (both containers Healthy; pg_isready accepting connections; redis-cli ping -> PONG)"
},
{
"criterion": "AC5: verify.sh green",
"test": "./scripts/verify.sh",
"result": "PASS (exit 0)"
}
],
"regressions": "PASS - F-001 suite re-run green: lint, typecheck, build, unit tests (11 passed | 6 skipped without DB), /health unaffected (no HTTP change)",
"evidence": [
"npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6)",
"npm run db:up (applied DB) -> No migrations to run!",
"docker:down + docker:up -> mdv-dev-postgres Healthy, mdv-dev-redis Healthy, pg_isready OK, redis PONG",
"npm run lint/typecheck/build/test -> all exit 0",
"./scripts/verify.sh -> exit 0"
],
"timestamp": "2026-08-14T20:02:00Z"
}

View File

@@ -0,0 +1,31 @@
{
"feature_id": "F-002",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "Database foundation matches specs/F-002 DESIGN.md. Migration tooling demonstrated against real PostgreSQL 16, pool helper fail-fast, compose one-command dev environment, naming convention documented. No blockers.",
"checks": {
"design_conformance": "PASS: src/infrastructure/db/pool.ts, migrations/001_baseline.js, docker-compose.yml, npm scripts all present as designed; --env-file-if-exists used instead of strict --env-file (safer, equivalent intent)",
"migration_lifecycle": "PASS (re-executed by reviewer): db:up applies 001_baseline; db:down reverts; db:up re-applies; second up is no-op",
"naming_convention": "PASS: <module>_<table> documented in README with rules; baseline table app_meta follows prefix rule; no business tables introduced",
"test_hygiene": "PASS: integration tests skip explicitly without TEST_DATABASE_URL (npm test green without DB); shared-DB race resolved by disabling fileParallelism with inline justification",
"boundary_rules": "PASS: lint:boundaries clean over 11 files; db code lives in infrastructure, no module touched"
},
"findings": [
{
"severity": "info",
"note": "node-pg-migrate programmatic runner needs explicit migrationsTable; test helper pins 'pgmigrations' to match CLI default. Documented in implementer.md known limitations."
},
{
"severity": "info",
"note": "F-004 will centralize config; current minimal fail-fast env reader in pool.ts is acceptable and called out as follow-up."
}
],
"evidence": [
"npm run lint / typecheck / lint:boundaries / test -> all exit 0",
"npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6) against PostgreSQL 16 in docker",
"reviewer re-run: db:down then db:up -> 001_baseline DOWN then UP, Migrations complete",
"docker exec mdv-dev-redis redis-cli ping -> PONG (from build stage)",
"files reviewed: project/migrations/001_baseline.js, src/infrastructure/db/**, docker-compose.yml, scripts/db-status.mjs, README.md, spec/tech.md"
],
"timestamp": "2026-08-14T19:57:00Z"
}

View File

@@ -0,0 +1,28 @@
{
"feature_id": "F-002",
"agent": "security",
"verdict": "APPROVED",
"summary": "Security gate passed. Zero audit vulnerabilities with new deps (pg, node-pg-migrate), no secrets in code, .env gitignored, parameterized queries everywhere, and the one DDL interpolation point is now guarded by strict identifier validation with regression tests.",
"checks": {
"secrets": "PASS: no hardcoded secrets in src/scripts/migrations; dev compose credentials are explicitly documented as dev-only; .env confirmed gitignored (git check-ignore .env)",
"dependencies": "PASS: npm audit -> 0 vulnerabilities; pg/node-pg-migrate/@types/pg justified in spec/tech.md",
"sast_basic": "PASS: no eval/new Function; template-literal SQL limited to 2 DDL statements in test support, guarded by /^[a-zA-Z_][a-zA-Z0-9_]*$/ validation in dbNameFromUrl (rejection covered by db-test-support.test.ts)",
"input_validation": "PASS: pool helper uses $n parameterized queries exclusively for data access",
"exposure": "INFO accepted: compose binds 5432/6379 on host for local dev only; documented in README"
},
"findings": [
{
"severity": "low",
"note": "DDL identifier interpolation in test support",
"resolution": "MITIGATED this gate round: strict identifier validation added before any DDL use + unit tests rejecting bad\"name and semi;colon cases"
}
],
"evidence": [
"npm audit -> found 0 vulnerabilities",
"git check-ignore .env -> ignored",
"grep secret scan over src/scripts/migrations -> none",
"grep for template-literal queries -> only the 2 guarded DDL statements remain",
"npm test after hardening -> 11 passed | 6 skipped; test:integration -> 6 passed"
],
"timestamp": "2026-08-14T20:00:00Z"
}