All findings from the inaugural LeadValidator audit resolved and confirmed. Release gate: PASS. VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering all 20 route groups (46 endpoints documented in docs/openapi/) VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts — replaced pool.query shim with unknown[] + Object.defineProperty, zero any types, eslint-disable suppressions removed VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and HealthDetailedController — injected AgentRepository/CredentialRepository and DbProbe interface respectively; added CredentialRepository.findActiveClientId() VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services — ComplianceStatusStore, EventPublisher, MarketplaceService, OIDCTrustPolicyService, UsageService VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route groups — analytics, billing, tiers, webhooks, marketplace, oidc-trust-policies, oidc-token-exchange VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4 OpenSpec archives — all archives now complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.0 KiB
VV_ISSUE_004 — Controllers directly access database pool (SRP + DRY violation)
Status: RESOLVED Severity: MAJOR Category: SOLID_VIOLATION Logged by: LeadValidator Date: 2026-04-07 Audit phase: Phase E — SOLID Principles Audit / Phase D — DRY Principle Audit
Finding
The PRD (Section 6.2 — SOLID, Section 6.1 — DRY, Section 8) states:
"DB queries |
src/services/| All database access" "No business logic in controllers" "Services depend on abstractions — no direct instantiation of dependencies in business logic"
Two controllers bypass the repository/service layer entirely and execute raw SQL queries directly against the PostgreSQL pool:
-
ScaffoldController— executespool.query()directly to fetch agent and credential data (two raw SQL queries). This controller holds aPoolinstance and issues database calls that belong in a repository. -
HealthDetailedController— executespool.connect()andpool.query('SELECT 1')directly to check database liveness. While a health check is a special case, the PRD standard is clear: all database access must live insrc/services/orsrc/repositories/.
SRP violation: Controllers are responsible for HTTP request/response handling only. They must not contain data access logic.
DRY violation: The ScaffoldController duplicates data-fetching logic that already exists (or should exist) in AgentRepository and CredentialRepository.
Evidence
src/controllers/ScaffoldController.ts, lines 56–82:
const agentResult = await this.pool.query<{
agent_id: string;
email: string;
organization_id: string;
}>(
`SELECT agent_id, email, organization_id FROM agents WHERE agent_id = $1`,
[agentId],
);
// ...
const credResult = await this.pool.query<{ client_id: string }>(
`SELECT client_id FROM credentials WHERE agent_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`,
[agentId],
);
src/controllers/HealthDetailedController.ts, lines 121–123:
const client = await this.pool.connect();
// ...
await client.query('SELECT 1');
Required Action
-
ScaffoldController: Move the two raw SQL queries into the appropriate repositories (
AgentRepository.findById()already exists — use it; addCredentialRepository.findActiveByAgentId()if not present). Inject repositories via constructor rather than the raw pool. -
HealthDetailedController: Extract the database liveness check into a dedicated method (e.g.,
HealthRepository.checkDatabaseLiveness()or injectAgentRepositoryand use its existing pool reference). Remove the rawPoolinjection from this controller.
The goal is that no controller ever holds a reference to a Pool object.
CTO Response
Confirmed. Both controllers violated SRP by holding a raw Pool reference. Fixed by:
ScaffoldController— injectingAgentRepository+CredentialRepository(both already existed in app.ts). AddedCredentialRepository.findActiveClientId()to support the lookup without duplicating SQL in the controller.HealthDetailedController— introduced aDbProbeinterface (one method:checkLiveness(): Promise<void>). ThePooladapter is created inhealth.ts(the route factory), so the controller never touchesPooldirectly.
Resolution
Files modified:
src/controllers/ScaffoldController.ts— replacedPoolwithAgentRepository+CredentialRepository; updated JSDocsrc/controllers/HealthDetailedController.ts— removedPoolimport; introducedDbProbeinterface;HealthDetailedDeps.pool→HealthDetailedDeps.dbProbesrc/routes/health.ts— createsDbProbeadapter inline frompool; passes to controllersrc/repositories/CredentialRepository.ts— addedfindActiveClientId(agentId): Promise<string | null>src/app.ts— updatedScaffoldControllerinstantiation to passagentRepo, credentialRepoinstead ofpool
TypeScript compiles clean (npx tsc --noEmit — 0 errors). No controller holds a Pool reference.