import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const TransactionSummarySchema = z.object({
userId: z.string().uuid(),
totalAmount: z.number().transform((val) => Number(val)),
transactionCount: z.number(),
});
export async function getMonthlySummary(minAmount: number) {
// Prisma parametrizes ${minAmount} safely under the hood
const results = await prisma.$queryRaw<unknown[]>`
SELECT "userId", SUM(amount) as "totalAmount", COUNT(*) as "transactionCount"
FROM "Transaction"
WHERE status = 'COMPLETED'
GROUP BY "userId"
HAVING SUM(amount) > ${minAmount}
`;
// Validate and transform types at runtime
return z.array(TransactionSummarySchema).parse(results);
}Să fim sinceri: Prisma e genială până când ai de făcut un raport complex sau un query cu window functions. Am pățit asta recent la un proiect cu vreo 12k utilizatori activi, unde trebuia să calculez niște medii mobile pe tranzacții. ORM-ul clasic dădea rateuri sau genera un SQL atât de mizerabil încât baza de date plângea de oboseală, așa că am coborât la $queryRaw.
Dar odată ce lași în urmă siguranța oferită de metodele standard, intri pe un teritoriu minat. Două probleme mari apar imediat: securitatea și iluzia tipurilor sigure din TypeScript.
Capcana interpolării și SQL Injection
Prisma te protejează implicit dacă folosești tag-ul dedicat pentru template literals. Când scrii prisma.$queryRawSELECT * FROM "User" WHERE email = ${email}``, Prisma nu face o simplă concatenare de string-uri. În spate, ea transformă totul într-un query parametrizat trimis direct către Postgres sau MySQL.
Dezastrul apare când ai nevoie de sortare dinamică sau de nume de coloane dinamice. SQL-ul nativ nu acceptă parametri pentru identificatori (nume de tabele sau coloane). Atunci, mulți devi fac greșeala să treacă la $queryRawUnsafe și să concateneze direct ce vine din request: req.query.sortBy.
Am văzut asta la un audit de cod la o aplicație fintech. Dacă un atacator trimite id; DROP TABLE "User";--, ai pierdut datele. Trade-off-ul este simplu: dacă ai neapărat nevoie de dynamic SQL, folosește un whitelist strict. Verifici manual dacă valoarea trimisă de user se află într-o listă predefinită de coloane permise înainte de a o concatena în query.
De ce genericele din Prisma sunt o minciună
Prisma îți permite să scrii ceva de genul prisma.$queryRaw<UserReport[]>...``. Pare frumos, dar este extrem de periculos. TypeScript este un limbaj de design-time. Acea conversie de tip este doar un simplu type cast sub capotă. Dacă baza de date returnează un câmp ca string (cum se întâmplă des cu tipurile DECIMAL sau BigInt în Postgres) în loc de number, TypeScript nu va zice nimic la compilare.
Aplicația ta va crăpa direct în producție când un serviciu va încerca să facă calcule matematice pe acel string.
Scutul de protecție numit Zod
Pentru a rezolva asta, eu folosesc Zod ca un filtru de runtime. În loc să mint compilatorul, îi spun lui Prisma că rezultatul este unknown[], iar apoi îl trec prin schema de validare Zod.
Zod nu doar că validează tipurile, dar le poate și transforma. De exemplu, un câmp de tip Numeric din Postgres vine ca string în JS. Cu Zod îl poți transforma automat în number folosind .transform(Number) chiar în momentul parsării.
Există totuși un mic trade-off de performanță aici. Pe un set de date uriaș, de zeci de mii de rânduri, parsarea cu Zod adaugă un overhead de CPU. Într-un test pe care l-am făcut cu 15k de înregistrări, parsarea a adăugat în jur de 12ms. Pentru majoritatea aplicațiilor web, acest cost este neglijabil în comparație cu siguranța pe care o primești.
Voi cum gestionați query-urile complexe în Prisma? Rămâneți pe $queryRaw sau preferați să mutați logica grea în View-uri sau proceduri stocate în baza de date?