feat(api): master-login stub + WorkspacePrismaPool (Frente E)

- Prisma 7: prisma.config.ts com datasource.url (API correta); schema gerado em CJS
- WorkspacePrismaPool: LRU cache (max 10) de PrismaClient por workspace (ADR 0006)
  PrismaPg adapter + pg.Pool por workspace; getOrCreate/health/onModuleDestroy
- JwtAuthGuard: global APP_GUARD, jose HS256, popula CLS com workspace_id/userId/prisma
  @Public() decorator marca ping/health/dev-auth como rotas abertas
- DevAuthController: POST /auth/dev/token — emite JWT dev (404 em produção)
- AuthTokenResponseSchema + DevTokenRequestSchema em @sar/api-interface
- WorkspacePoolHealthIndicator: health/ready reporta amostra LRU top-3 (nunca O(N))
- .npmrc: hoist @prisma/client-runtime-utils (requerido pelo Prisma 7 isolated mode)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 22:36:00 +00:00
parent bca2e3ebb3
commit 2a8be3fd82
22 changed files with 1204 additions and 39 deletions

View File

@@ -5,10 +5,13 @@ import { randomUUID } from 'node:crypto';
import type { Request, Response } from 'express';
import type { WorkspaceClsStore } from './workspace.types';
import type { Env } from '../config/env.schema';
import { WorkspacePrismaPool } from './workspace-prisma-pool.service';
// CLS popula contexto por request. Hoje: requestId + DEFAULT_WORKSPACE_ID do env.
// Amanhã: workspaceId vem do JWT (PGD-AUTHZ-002); `prisma` é resolvido pelo
// WorkspacePrismaPool e injetado via cls.set('prisma', ...) aqui mesmo.
// CLS middleware roda ANTES dos guards (ordem NestJS).
// Aqui: apenas requestId + workspaceId default.
// JwtAuthGuard atualiza workspaceId, userId e prisma após validar o token.
// CODING-RULES PGD-DB-009: prisma via cls.get('prisma'), nunca singleton.
// CODING-RULES PGD-AUTHZ-002: workspaceId real vem do JWT (guard), não do env.
@Module({
imports: [
@@ -21,28 +24,28 @@ import type { Env } from '../config/env.schema';
mount: true,
generateId: true,
idGenerator: (req: Request) => {
// Prioridade: req.id (pino-http já gerou/leu header) > header bruto > novo UUID.
const fromPino = (req as Request & { id?: unknown }).id;
if (typeof fromPino === 'string' && fromPino.length > 0) return fromPino;
const headerVal = req.headers['x-request-id'];
return typeof headerVal === 'string' && headerVal.length > 0
? headerVal
: randomUUID();
return typeof headerVal === 'string' && headerVal.length > 0 ? headerVal : randomUUID();
},
setup: (cls, req: Request, res: Response) => {
const store = cls as unknown as {
set: <K extends keyof WorkspaceClsStore>(key: K, value: WorkspaceClsStore[K]) => void;
getId: () => string;
};
const requestId = store.getId();
res.setHeader('x-request-id', requestId);
store.set('requestId', requestId);
// Fallback para rotas públicas (ping, health). Guard sobrescreve em rotas protegidas.
store.set('workspaceId', config.get('DEFAULT_WORKSPACE_ID', { infer: true }));
},
},
}),
}),
],
exports: [ClsModule],
providers: [WorkspacePrismaPool],
exports: [ClsModule, WorkspacePrismaPool],
})
export class WorkspaceModule {}