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:
@@ -1,28 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { APP_FILTER, APP_GUARD, APP_PIPE } from '@nestjs/core';
|
||||
import { ZodValidationPipe } from 'nestjs-zod';
|
||||
import { EnvModule } from './config/env.module';
|
||||
import { LoggerModule } from './logger/logger.module';
|
||||
import { WorkspaceModule } from './workspace/workspace.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { PingModule } from './ping/ping.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { JwtAuthGuard } from './auth/jwt-auth.guard';
|
||||
import { ProblemDetailsFilter } from './filters/problem-details.filter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Ordem importa: Env primeiro (fail-fast), depois Logger, CLS, módulos de domínio.
|
||||
// Ordem: Env primeiro (fail-fast), depois Logger, CLS, Auth, módulos de domínio.
|
||||
EnvModule,
|
||||
LoggerModule,
|
||||
WorkspaceModule,
|
||||
AuthModule,
|
||||
HealthModule,
|
||||
PingModule,
|
||||
],
|
||||
providers: [
|
||||
// Pipe global: nestjs-zod converte ZodSchema (via createZodDto) em validação automática.
|
||||
// CODING-RULES §06: schema é o contrato; DTO é a classe que o expõe.
|
||||
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
||||
// Filter global: RFC 9457. Zod → 422.
|
||||
{ provide: APP_FILTER, useClass: ProblemDetailsFilter },
|
||||
// Guard global — exige JWT em todas as rotas exceto as com @Public().
|
||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
12
apps/api/src/app/auth/auth.module.ts
Normal file
12
apps/api/src/app/auth/auth.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
import { DevAuthController } from './dev-auth.controller';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceModule],
|
||||
controllers: [DevAuthController],
|
||||
providers: [JwtAuthGuard],
|
||||
exports: [JwtAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
46
apps/api/src/app/auth/dev-auth.controller.ts
Normal file
46
apps/api/src/app/auth/dev-auth.controller.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, NotFoundException, Post } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SignJWT } from 'jose';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { DevTokenRequestSchema, type AuthTokenResponse } from '@sar/api-interface';
|
||||
import type { Env } from '../config/env.schema';
|
||||
import { Public } from './public.decorator';
|
||||
|
||||
class DevTokenRequestDto extends createZodDto(DevTokenRequestSchema) {}
|
||||
|
||||
// Dev-only stub — emite JWT HS256 para smoke tests locais.
|
||||
// CODING-RULES PGD-SEC-002: retorna 404 em produção.
|
||||
// CODING-RULES PGD-AUTHZ-002: workspace_id vem do body aqui APENAS porque
|
||||
// este endpoint É o gerador do token — nenhum outro handler pode fazer isso.
|
||||
|
||||
@Public()
|
||||
@Controller({ path: 'auth/dev' })
|
||||
export class DevAuthController {
|
||||
private readonly secret: Uint8Array;
|
||||
private readonly expiresIn: number;
|
||||
private readonly isProd: boolean;
|
||||
|
||||
constructor(config: ConfigService<Env, true>) {
|
||||
this.secret = new TextEncoder().encode(config.get('MASTER_LOGIN_JWT_SECRET', { infer: true }));
|
||||
this.expiresIn = config.get('JWT_ACCESS_EXPIRATION', { infer: true });
|
||||
this.isProd = config.get('NODE_ENV', { infer: true }) === 'production';
|
||||
}
|
||||
|
||||
@Post('token')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async token(@Body() dto: DevTokenRequestDto): Promise<AuthTokenResponse> {
|
||||
if (this.isProd) throw new NotFoundException();
|
||||
|
||||
const accessToken = await new SignJWT({
|
||||
workspace_id: dto.workspaceId,
|
||||
role: dto.role,
|
||||
})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setSubject(dto.userId)
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(`${this.expiresIn}s`)
|
||||
.sign(this.secret);
|
||||
|
||||
return { accessToken, tokenType: 'Bearer', expiresIn: this.expiresIn };
|
||||
}
|
||||
}
|
||||
79
apps/api/src/app/auth/jwt-auth.guard.ts
Normal file
79
apps/api/src/app/auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { jwtVerify } from 'jose';
|
||||
import type { Request } from 'express';
|
||||
import type { Env } from '../config/env.schema';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { WorkspacePrismaPool } from '../workspace/workspace-prisma-pool.service';
|
||||
import type { JwtPayload } from './jwt.types';
|
||||
import { IS_PUBLIC_KEY } from './public.decorator';
|
||||
|
||||
// Guard global (APP_GUARD). Valida Bearer HS256 e atualiza CLS com workspace real.
|
||||
// CODING-RULES PGD-AUTHZ-002: workspaceId sempre do JWT, nunca de body/param.
|
||||
// Ordem NestJS: middleware CLS (workspace default) → este guard (workspace real).
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
private readonly secret: Uint8Array;
|
||||
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly cls: ClsService<WorkspaceClsStore>,
|
||||
private readonly pool: WorkspacePrismaPool,
|
||||
config: ConfigService<Env, true>,
|
||||
) {
|
||||
this.secret = new TextEncoder().encode(config.get('MASTER_LOGIN_JWT_SECRET', { infer: true }));
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (this.isPublic(context)) return true;
|
||||
|
||||
const req = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractBearer(req);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('token ausente');
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify<JwtPayload>(token, this.secret, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
|
||||
(req as Request & { user: JwtPayload }).user = payload as JwtPayload;
|
||||
|
||||
// Sobrescreve CLS com workspace real do JWT (corre depois do middleware).
|
||||
const workspaceId = payload.workspace_id;
|
||||
this.cls.set('workspaceId', workspaceId);
|
||||
this.cls.set('userId', payload.sub);
|
||||
|
||||
const dbUrl =
|
||||
process.env['DATABASE_URL'] ??
|
||||
`postgresql://sar:sar_dev_password@localhost:5432/sar_workspace_${workspaceId}`;
|
||||
this.cls.set('prisma', this.pool.getOrCreate(workspaceId, dbUrl));
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException('token inválido ou expirado');
|
||||
}
|
||||
}
|
||||
|
||||
private isPublic(ctx: ExecutionContext): boolean {
|
||||
return (
|
||||
this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
ctx.getHandler(),
|
||||
ctx.getClass(),
|
||||
]) === true
|
||||
);
|
||||
}
|
||||
|
||||
private extractBearer(req: Request): string | undefined {
|
||||
const auth = req.headers['authorization'];
|
||||
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
||||
return auth.slice(7);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
17
apps/api/src/app/auth/jwt.types.ts
Normal file
17
apps/api/src/app/auth/jwt.types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Claims do JWT emitido pelo master-login. Fonte da verdade para req.user.
|
||||
// CODING-RULES PGD-AUTHZ-002: workspace_id vem sempre do token, nunca do body.
|
||||
|
||||
export type JwtRole = 'rep' | 'supervisor' | 'manager' | 'admin';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string; // userId
|
||||
workspace_id: string;
|
||||
role: JwtRole;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
// Tipo auxiliar para requests autenticados — evita global namespace augmentation.
|
||||
export interface AuthenticatedRequest {
|
||||
user: JwtPayload;
|
||||
}
|
||||
6
apps/api/src/app/auth/public.decorator.ts
Normal file
6
apps/api/src/app/auth/public.decorator.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
|
||||
/** Marca um controller ou handler como público — JwtAuthGuard não exige token. */
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -5,22 +5,22 @@ import {
|
||||
HealthCheckService,
|
||||
MemoryHealthIndicator,
|
||||
} from '@nestjs/terminus';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
import { WorkspacePoolHealthIndicator } from './workspace-pool.health-indicator';
|
||||
|
||||
// CODING-RULES §20 (PGD-OBS-003):
|
||||
// /health/live → liveness só com memory.checkHeap(350MB).
|
||||
// /health/ready → readiness pinga master-login + amostra LRU (K=3) dos pools
|
||||
// quentes do WorkspacePrismaPool + Valkey + BullMQ.
|
||||
// NUNCA percorrer todos os workspaces (O(N) → false negative).
|
||||
//
|
||||
// Hoje o "ready" só checa heap, idêntico ao live. Quando master-login,
|
||||
// WorkspacePrismaPool, Valkey e BullMQ entrarem, cada um adiciona seu indicator
|
||||
// aqui — sem nunca virar O(N) sobre workspaces.
|
||||
// /health/live → liveness: memory.checkHeap(350MB).
|
||||
// /health/ready → readiness: heap + amostra LRU (K=3) do WorkspacePrismaPool.
|
||||
// Próximos: MasterLoginHealthIndicator, ValkeyHealthIndicator, BullMQHealthIndicator.
|
||||
// NUNCA percorrer todos os workspaces (O(N)).
|
||||
|
||||
@Public()
|
||||
@Controller({ path: 'health' })
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly health: HealthCheckService,
|
||||
private readonly memory: MemoryHealthIndicator,
|
||||
private readonly workspacePool: WorkspacePoolHealthIndicator,
|
||||
) {}
|
||||
|
||||
@Get('live')
|
||||
@@ -32,11 +32,9 @@ export class HealthController {
|
||||
@Get('ready')
|
||||
@HealthCheck()
|
||||
ready(): Promise<HealthCheckResult> {
|
||||
// Skeleton: por enquanto idêntico ao live. Próximas frentes:
|
||||
// - MasterLoginHealthIndicator (obrigatório)
|
||||
// - WorkspacePoolLruHealthIndicator (K=3 amostra)
|
||||
// - ValkeyHealthIndicator
|
||||
// - BullMQHealthIndicator
|
||||
return this.health.check([() => this.memory.checkHeap('heap', 350 * 1024 * 1024)]);
|
||||
return this.health.check([
|
||||
() => this.memory.checkHeap('heap', 350 * 1024 * 1024),
|
||||
() => this.workspacePool.check('workspace_pool', 3),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { HealthController } from './health.controller';
|
||||
import { WorkspacePoolHealthIndicator } from './workspace-pool.health-indicator';
|
||||
|
||||
@Module({
|
||||
imports: [TerminusModule],
|
||||
imports: [TerminusModule, WorkspaceModule],
|
||||
controllers: [HealthController],
|
||||
providers: [WorkspacePoolHealthIndicator],
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
||||
35
apps/api/src/app/health/workspace-pool.health-indicator.ts
Normal file
35
apps/api/src/app/health/workspace-pool.health-indicator.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HealthIndicatorResult, HealthIndicator, HealthCheckError } from '@nestjs/terminus';
|
||||
import { WorkspacePrismaPool } from '../workspace/workspace-prisma-pool.service';
|
||||
|
||||
// Amostra os K workspaces mais quentes do LRU — nunca O(N) sobre todos os workspaces.
|
||||
// CODING-RULES §20 (PGD-OBS-003): readiness não percorre workspaces individualmente.
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePoolHealthIndicator extends HealthIndicator {
|
||||
constructor(private readonly pool: WorkspacePrismaPool) {
|
||||
super();
|
||||
}
|
||||
|
||||
async check(key = 'workspace_pool', k = 3): Promise<HealthIndicatorResult> {
|
||||
const results = await this.pool.health(k);
|
||||
|
||||
if (results.length === 0) {
|
||||
return this.getStatus(key, true, { active: 0 });
|
||||
}
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
const isHealthy = failed.length === 0;
|
||||
const detail = {
|
||||
active: results.length,
|
||||
healthy: results.length - failed.length,
|
||||
...(failed.length > 0 && { failed: failed.map((r) => r.workspaceId) }),
|
||||
};
|
||||
|
||||
if (!isHealthy) {
|
||||
throw new HealthCheckError(`${key} degradado`, this.getStatus(key, false, detail));
|
||||
}
|
||||
|
||||
return this.getStatus(key, true, detail);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Controller, Get } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { PingResponse } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
|
||||
// Endpoint de verificação de fundação:
|
||||
// - confirma que CLS está populando workspaceId + requestId;
|
||||
@@ -9,6 +10,7 @@ import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
// - usado pela Web (Frente B) para validar conectividade real.
|
||||
// Contrato: @sar/api-interface · PingResponseSchema (zod).
|
||||
|
||||
@Public()
|
||||
@Controller({ path: 'ping' })
|
||||
export class PingController {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
|
||||
80
apps/api/src/app/workspace/workspace-prisma-pool.service.ts
Normal file
80
apps/api/src/app/workspace/workspace-prisma-pool.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import pg from 'pg';
|
||||
|
||||
// ADR 0006: BD-por-workspace — um PrismaClient por workspaceId, nunca singleton.
|
||||
// CODING-RULES PGD-DB-009: callers obtêm o client via CLS, não injetando este serviço.
|
||||
|
||||
const MAX_ENTRIES = 10; // LRU cap; ajustável via env na próxima iteração
|
||||
const PG_POOL_SIZE = 5;
|
||||
const noop = (): void => undefined;
|
||||
|
||||
interface PoolEntry {
|
||||
client: PrismaClient;
|
||||
pgPool: pg.Pool;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePrismaPool implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(WorkspacePrismaPool.name);
|
||||
|
||||
// Map preserves insertion order → LRU: primeiro = mais antigo, último = mais recente
|
||||
private readonly cache = new Map<string, PoolEntry>();
|
||||
|
||||
getOrCreate(workspaceId: string, dbUrl: string): PrismaClient {
|
||||
const hit = this.cache.get(workspaceId);
|
||||
if (hit) {
|
||||
// Move para o fim (LRU refresh)
|
||||
this.cache.delete(workspaceId);
|
||||
this.cache.set(workspaceId, hit);
|
||||
return hit.client;
|
||||
}
|
||||
|
||||
if (this.cache.size >= MAX_ENTRIES) {
|
||||
this.evictOldest();
|
||||
}
|
||||
|
||||
const pgPool = new pg.Pool({ connectionString: dbUrl, max: PG_POOL_SIZE });
|
||||
const adapter = new PrismaPg(pgPool);
|
||||
const client = new PrismaClient({ adapter });
|
||||
this.cache.set(workspaceId, { client, pgPool });
|
||||
this.logger.log(`pool criado: workspace=${workspaceId} total=${this.cache.size}`);
|
||||
return client;
|
||||
}
|
||||
|
||||
async health(k = 3): Promise<{ workspaceId: string; ok: boolean; latencyMs?: number }[]> {
|
||||
// Verifica os k workspaces mais recentes
|
||||
const entries = [...this.cache.entries()].slice(-k);
|
||||
return Promise.all(
|
||||
entries.map(async ([workspaceId, { pgPool }]) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const conn = await pgPool.connect();
|
||||
conn.release();
|
||||
return { workspaceId, ok: true, latencyMs: Date.now() - start };
|
||||
} catch {
|
||||
return { workspaceId, ok: false };
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
[...this.cache.values()].map(({ client, pgPool }) =>
|
||||
client.$disconnect().finally(() => pgPool.end()),
|
||||
),
|
||||
);
|
||||
this.cache.clear();
|
||||
this.logger.log('pool destruído — todos os clientes desconectados');
|
||||
}
|
||||
|
||||
private evictOldest(): void {
|
||||
const [oldestId, oldest] = this.cache.entries().next().value as [string, PoolEntry];
|
||||
void oldest.client.$disconnect().catch(noop);
|
||||
void oldest.pgPool.end().catch(noop);
|
||||
this.cache.delete(oldestId);
|
||||
this.logger.log(`evicted LRU workspace=${oldestId}`);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ClsStore } from 'nestjs-cls';
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Forma do CLS store por request — fonte da verdade para qualquer caller
|
||||
// que faça `cls.get(...)`. Quando o PrismaClient por workspace entrar
|
||||
// (ADR 0006), `prisma` virará obrigatório aqui — por hora segue opcional.
|
||||
// Forma do CLS store por request — fonte da verdade para qualquer caller.
|
||||
// CODING-RULES PGD-DB-009: nunca importe PrismaClient diretamente; use cls.get('prisma').
|
||||
// CODING-RULES PGD-AUTHZ-002: workspaceId vem sempre do JWT, nunca de body/param/query.
|
||||
|
||||
export interface WorkspaceClsStore extends ClsStore {
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
// userId virá quando master-login estiver plugado.
|
||||
userId?: string;
|
||||
// prisma: PrismaClient — adicionar quando WorkspacePrismaPool entrar.
|
||||
userId?: string; // preenchido pelo JwtAuthGuard (M3)
|
||||
prisma?: PrismaClient; // preenchido pelo WorkspaceModule após WorkspacePrismaPool entrar (M5)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user