import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const ActivitySchema = z.object({
id: z.string(),
action: z.string(),
created_at: z.coerce.date(),
user_email: z.string().email(),
});
export async function getRecentActivity(userId: string, limit: number) {
// Prisma parametrizează automat variabilele trimise în template literal
const rawData = await prisma.$queryRaw<unknown[]>`
SELECT a.id, a.action, a.created_at, u.email as user_email
FROM "Activity" a
JOIN "User" u ON a.user_id = u.id
WHERE a.user_id = ${userId}
ORDER BY a.created_at DESC
LIMIT ${limit}
`;
// Validăm datele la runtime. Dacă baza returnează altceva, crapă aici, nu în UI
return z.array(ActivitySchema).parse(rawData);
}Prisma e minunat până când te lovești de un raport complex cu window functions sau un search cu vectori unde ORM-ul pur și simplu își prinde urechile. Atunci cobori în mină cu $queryRaw și speri să nu aduci în producție vreo vulnerabilitate SQL injection sau tipuri de any. Hai să-ți arăt cum am rezolvat asta curat pe un proiect cu peste 150k de useri activi.
Când am dat skip la ORM și am trecut pe raw
La un proiect trecut, aveam de generat un feed de activitate personalizat. Prisma genera un SQL cu 12 JOIN-uri și un query plan care îmi punea baza de date în cap la doar 50 de request-uri concurente. Timpul de răspuns era undeva la 1.8 secunde. Groaznic.
Am rescris totul în SQL pur folosind LATERAL JOIN și window functions. Timpul de răspuns a scăzut la 45ms. Trade-off-ul? Am pierdut instantaneu type-safety-ul pe care Prisma ți-l oferă implicit și a trebuit să scriu mapări manuale pentru rezultatele întoarse din baza de date.
SQL Injection și iluzia de siguranță
Multă lume crede că dacă folosește $queryRaw e safe fiindcă Prisma face magie cu template strings. Da, Prisma folosește tagged templates ca să trimită parametrii separat ca query placeholders.
Problema apare când ai nevoie de sorting dinamic sau filtre opționale. Am văzut destule cazuri în care programatorii apelau la Prisma.raw() ca să concateneze bucăți de query primite direct din request query params. Acolo ești expus direct la SQL Injection. Regula de aur: folosește parametrizarea nativă pentru valori și sanitizează agresiv (folosind whitelist-uri) numele de coloane sau direcțiile de sortare (ASC/DESC).
Cum facem totul type-safe cu Zod
Când scrii $queryRaw<UserActivity[]> în Prisma, tu de fapt minți compilatorul de TypeScript. Îi spui: „Crede-mă pe cuvânt că baza de date returnează exact structura asta”. Dacă schimbi o coloană în DB și uiți să updatezi tipul din cod, crapă în producție cu un undefined silențios.
TypeScript dispare la runtime. Dacă baza ta de date returnează un Int în loc de BigInt sau un câmp e null pe ascuns, tipurile tale statice nu te vor salva. Zod acționează ca un scut de protecție la granița dintre baza de date și aplicația ta, asigurându-se că datele sunt exact în formatul așteptat înainte să ajungă în UI sau în alte servicii.
Soluția pe care o folosesc acum e să nu mai folosesc genericele din Prisma pentru asta. În schimb, las query-ul să returneze unknown și trec rezultatul printr-o schemă Zod. Dacă schema dă fail, măcar crapă controlat în faza de parsing și știu exact ce s-a schimbat în schema bazei de date. Plus că Zod transformă datele (de exemplu, string-uri de dată în obiecte Date reale) direct la runtime.
E un pic de boilerplate în plus? Da, absolut. Dar pentru query-urile alea critice unde performanța contează, merită fiecare linie suplimentară. Voi cum gestionați query-urile complexe în Prisma? Rămâneți pe raw sau preferați să spargeți logica în mai multe query-uri din ORM?