import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const MonthlyReportSchema = z.object({
userId: z.string().uuid(),
totalAmount: z.coerce.number(),
transactionCount: z.coerce.number(),
});
type MonthlyReport = z.infer<typeof MonthlyReportSchema>;
async function getMonthlyReport(minAmount: number): Promise<MonthlyReport[]> {
// Prisma parametrizează automat ${minAmount} via Tagged Template
const rawData = await prisma.$queryRaw<unknown[]>`
SELECT
"userId",
SUM(amount) AS "totalAmount",
COUNT(id) AS "transactionCount"
FROM "Transaction"
WHERE amount > ${minAmount}
GROUP BY "userId"
HAVING COUNT(id) > 5
`;
// Validare la RUNTIME cu Zod pentru type-safety real
return z.array(MonthlyReportSchema).parse(rawData);
}Am lucrat recent la un modul de rapoarte financiare unde Prisma ORM dădea chix la nivel de performanță. Query-ul generat de abstracția lor făcea 4 JOIN-uri inutile și dura în jur de 1.8 secunde pe un tabel cu 250k tranzacții. Trecerea la $queryRaw a scăzut timpul de răspuns la 35ms, dar mi-a adus rapid două bătăi de cap: riscul de SQL injection și pierderea tipurilor TypeScript generate automat.
Când renunți la abstracția Prisma
Iubesc Prisma pentru DX-ul pe care ți-l oferă în 90% din cazuri, dar ORM-urile au limite clare. Cobor la SQL nativ când am nevoie de CTE-uri (Common Table Expressions), Window Functions (ROW_NUMBER(), RANK()), sau agregări complexe pe seturi mari de date.
Trade-off-ul e evident. Când scrii $queryRaw, renunți la independența față de baza de date și la refactoring-ul automat. Dacă schimbi numele unei coloane în schema.prisma, TypeScript nu o să țipe la tine că query-ul tău SQL scris de mână s-a stricat. E un compromis pe care trebuie să ți-l asumi pentru performanță.
SQL Injection: tagged templates vs string interpolation
Cea mai mare frică când cobori în SQL nativ e să nu lași o portiță deschisă pentru SQL Injection. Din fericire, echipa de la Prisma a gândit $queryRaw folosind ES6 Tagged Template Literals.
Când scrii prisma.$queryRawSELECT * FROM "User" WHERE email = ${email}``, JavaScript nu concatenează string-ul direct. Prisma primește template-ul spart în bucăți și trimite valorile ca parametri separați ($1, $2) către PostgreSQL sau MySQL. Baza de date sanitizează totul automat.
Unde ți-o furi? Când încerci să fii deștept și construiești query-ul prin concatenare manuală sau folosești funcții ajutătoare greșite:
// AȘA NU! Vulnerabil la SQL Injection
const query = `SELECT * FROM "User" WHERE email = '${email}'`;
await prisma.$queryRawUnsafe(query);
Regula mea e simplă: folosesc $queryRaw în 99% din cazuri. Niciodată $queryRawUnsafe, decât dacă chiar trebuie să trec dinamic nume de tabele sau coloane, caz în care le trec mai întâi printr-un whitelist strict în cod.
Type-safety pe bune folosind Zod
Aici este capcana în care cad cei mai mulți devi. Dacă scrii prisma.$queryRaw<User[]>...``, ai doar o iluzie de siguranță. Genericul ăla e pur compile-time magic — e un simplu type cast (as User[]). Dacă baza de date îți returnează un null unde te așteptai la valoare, sau dacă driver-ul de Postgres îți dă un BigInt (pe care JSON.stringify îl urăște), TypeScript tace și înghite. Codul va crăpa urât la runtime, direct în fața clientului.
Soluția pe care o aplic pe toate proiectele este validarea strictă cu Zod. Extrag rezultatul neprelucrat ca unknown[] și îl trec prin schema Zod. Așa prind instant orice neconcordanță dintre ce returnează SQL-ul și ce așteaptă aplicația. În plus, Zod te ajută enorm cu transformările — de exemplu, poți transforma automat un string sau BigInt din SQL într-un number curat în JS folosind z.coerce.number().
Dacă vrei performanță de SQL nativ, plătești biletul: scrii query-ul cu tagged templates și îl validezi la runtime cu Zod.
Voi cât de des coborâți la $queryRaw pe proiectele voastre sau preferați să mutați logica complexă în Stored Procedures și Views?