import { PrismaClient, Prisma } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
// 1. Definim schema Zod pentru datele întoarse din SQL brut
const MonthlyStatsSchema = z.array(
z.object({
userId: z.string().uuid(),
totalEvents: z.bigint().transform((val) => Number(val)), // fix BigInt serialization
lastSeen: z.date(),
})
);
type MonthlyStats = z.infer<typeof MonthlyStatsSchema>;
export async function getMonthlyStats(tenantId: string, minEvents: number): Promise<MonthlyStats> {
// 2. Query sigur prin tagged template literals (parametri bindați automat)
const rawData = await prisma.$queryRaw`
SELECT
user_id AS "userId",
COUNT(id) AS "totalEvents",
MAX(created_at) AS "lastSeen"
FROM audit_logs
WHERE tenant_id = ${tenantId}
GROUP BY user_id
HAVING COUNT(id) >= ${minEvents}
ORDER BY "totalEvents" DESC
LIMIT 50;
`;
// 3. Runtime validation: dacă Postgres trimite altceva, crapă aici, nu în frontend
return MonthlyStatsSchema.parse(rawData);
}Toți iubim Prisma pentru DX și migrări curate, dar oricine a lucrat pe date mai serioase a lovit măcar o dată zidul de performanță. Am avut cazul acum vreo opt luni pe un tabel de audit logs cu vreo 4.2 milioane de rânduri: un simplu dashboard de analytics cu două groupBy-uri și un JOIN dura 1.8 secunde și mânca 400MB RAM în containerul de Node doar din maparea obiectelor. Trecut pe SQL nativ via $queryRaw, timpul a scăzut la 75ms.
ORM-ul devine o frână când ai nevoie de window functions (ROW_NUMBER(), RANK()), operații pe JSONB, agregări compuse sau când Prisma Engine insistă să facă join-urile în memorie (faimosul split query pe relații mari). Atunci n-ai ce face, cobori la nivelul solului.
Pericolul invizibil: SQL Injection și Prisma.raw
Prisma te protejează implicit dacă folosești tagged template literals. Mecanismul transformă automat variabilele din șir în parametri bindați ($1, $2), așa că un query de genul prisma.$queryRaw\SELECT * FROM users WHERE email = ${userInput}`` este 100% sigur.
Problema începe când ai nevoie de clauze dinamice: sortare după o coloană aleasă de client, filtrare opțională sau paginare dinamică. Mulți dezvoltatori cad în capcana lui Prisma.raw() sau, mai rău, trec pe $queryRawUnsafe și folosesc string concatenation.
Regula mea e simplă: nu folosi niciodată $queryRawUnsafe. Dacă ai bucăți dinamice (cum ar fi direcția de sortare sau numele coloanei), folosește un whitelist strict înainte de Prisma.raw(), iar pentru clauze WHERE compuse folosește Prisma.join().
Problema tipurilor: minciuna cu as unknown as MyType[]
Cea mai mare mizerie la $queryRaw e că întoarce unknown. Ce face 90% din lumea pe care o văd în code reviews? Trântește un cast direct:
const users = await prisma.$queryRaw<UserSummary[]>\`...\`;
Chestia asta e o iluzie. TypeScript doar te lasă în pace la compile time, dar în runtime ești expus la surprize clasice de Postgres. De exemplu, un COUNT(*) sau un câmp BIGINT vine în JavaScript ca obiect de tip BigInt nativ (care crapă instant dacă încerci să-l serializezi cu JSON.stringify()). Câmpurile nullable agregate vin ca null, nu ca undefined, iar dacă redenumești o coloană în schema SQL dar uiți de interfață, aplicația crapă silențios fix în producție.
Soluția solidă: Runtime parsing cu Zod
În loc să minți compilatorul cu type assertion, pasezi rezultatul direct printr-o schemă Zod. Da, adaugă un micro-overhead de CPU la parsare, dar e neglijabil comparativ cu liniștea că datele tale sunt valide și serializabile.
Trade-off-ul e evident: pierzi sincronizarea automată cu schema.prisma. Dacă redenumești o coloană în baza de date, $queryRaw nu-ți va da eroare la prisma generate, ci abia în teste sau în runtime când schema Zod dă fail la parsare. E un preț pe care merită să-l plătești pentru viteză brută, dar recomand să ții astfel de query-uri izolate într-un layer de repository și acoperite întotdeauna de teste de integrare.
Voi cum gestionați coloanele dinamice de sortare în Prisma fără să riscați injection?