mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
Phase 0 — NestJS + Zod foundation harness (F1–F8) (#1050)
Co-hosted NestJS app behind the existing Express server via a strangler-fig dispatcher, sharing the same better-sqlite3 connection and JWT httpOnly cookie. Additive and dormant: default routing stays on Express, Nest only serves its own /api/_nest diagnostics until a module opts in. F1 @trek/shared Zod contract package; F2 Nest bootstrap co-hosted (fall-through, single Dockerfile/port); F3 shared better-sqlite3 provider; F4 JWT cookie auth guard (+ @CurrentUser, admin guard); F5 Zod validation pipe + error-envelope parity; F6 Nest test + coverage gates; F7 per-prefix strangler toggle (env, default Express); F8 CI build/typecheck/test/coverage. Remaining F4/F6/F8 checklist items (trip-access + permission levels + MFA policy, e2e harness/seed + 80% gate, Nest↔Express parity test, Playwright PR-comment workflow) are tracked on the first consuming module cards (L1/A1/C1).
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
|
||||
/**
|
||||
* Normalises every Nest exception to TREK's legacy error envelope so migrated
|
||||
* routes are byte-identical for the client:
|
||||
* - 4xx -> { error: <message> } (5xx -> { error: 'Internal server error' })
|
||||
* - exceptions already throwing { error, code? } (e.g. the auth guards) pass through
|
||||
* This replaces Nest's default { statusCode, message, error } body, which the
|
||||
* TREK client does not expect.
|
||||
*/
|
||||
@Catch()
|
||||
export class TrekExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const res = host.switchToHttp().getResponse<Response>();
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
const status = exception.getStatus();
|
||||
const body = exception.getResponse();
|
||||
|
||||
// Already in TREK shape (e.g. guards throw { error, code }): pass through.
|
||||
if (body && typeof body === 'object' && 'error' in (body as Record<string, unknown>)) {
|
||||
res.status(status).json(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = typeof body === 'string' ? body : (body as { message?: unknown })?.message;
|
||||
const message =
|
||||
status < 500
|
||||
? Array.isArray(raw)
|
||||
? raw.join(', ')
|
||||
: String(raw ?? 'Error')
|
||||
: 'Internal server error';
|
||||
res.status(status).json({ error: message });
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown/unhandled error — mirror the legacy 500 behaviour.
|
||||
console.error('Unhandled error:', exception);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ArgumentMetadata, HttpException, Injectable, PipeTransform } from '@nestjs/common';
|
||||
import type { ZodType } from 'zod';
|
||||
|
||||
/**
|
||||
* Validates an incoming @Body()/@Query() against a Zod schema (from @trek/shared)
|
||||
* and returns the parsed, typed value. On failure it throws TREK's error envelope
|
||||
* `{ error: string }` with status 400 — the same shape the legacy routes produce,
|
||||
* so the client's error handling is unaffected.
|
||||
*
|
||||
* Usage: `@Body(new ZodValidationPipe(someSchema)) dto: Dto`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ZodValidationPipe implements PipeTransform {
|
||||
constructor(private readonly schema: ZodType) {}
|
||||
|
||||
transform(value: unknown, _metadata: ArgumentMetadata): unknown {
|
||||
const result = this.schema.safeParse(value);
|
||||
if (!result.success) {
|
||||
const message = result.error.issues
|
||||
.map((i) => `${i.path.join('.') || 'body'}: ${i.message}`)
|
||||
.join('; ');
|
||||
throw new HttpException({ error: message }, 400);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user