import { prisma } from './db';
import { z } from 'zod';
const UserReportSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
total_spent: z.union([z.string(), z.number()]).transform((val) => Number(val)),
});
type UserReport = z.infer<typeof UserReportSchema>;
async function getTopSpenders(minAmount: number): Promise<UserReport[]> {
const rawData = await prisma.$queryRaw`
SELECT u.id, u.email, SUM(o.amount) as total_spent
FROM "User" u
JOIN "Order" o ON o."userId" = u.id
GROUP BY u.id, u.email
HAVING SUM(o.amount) > ${minAmount}
ORDER BY total_spent DESC
LIMIT 10;
`;
return z.array(UserReportSchema).parse(rawData);
}Dacă folosești Prisma de ceva timp, sigur ai dat de un raport sau de un query complex unde ORM-ul pur și simplu a îngenuncheat sau a generat un SQL monstruos de ineficient. În loc să te chinui cu workaround-uri dubioase, soluția e $queryRaw. În postarea asta scurtă îți arăt cum scrii SQL brut în siguranță și cum recuperezi type-safety-ul folosind Zod.
De ce crapă Prisma la chestii grele și când coborâm în "măruntaie"
Am avut acum câteva luni un task pe un proiect cu vreo 1.5 milioane de înregistrări în tabela de tranzacții. Trebuia să scot un raport agregat cu ferestre temporale (window functions) și niște JOIN-uri condiționate destul de urâte. Am încercat inițial cu API-ul nativ de Prisma. Rezultatul? Un query generat care rula în 14 secunde și consuma RAM-ul containerului de parca era pe gratis.
Am coborât la $queryRaw și am scris SQL curat, optimizat cu indecși dedicați. Timpul de rulare a scăzut la 120ms (o reducere masivă de performanță). Dar am pierdut imediat schema de TypeScript pe care Prisma o generează automat. Aici intervine problema: cum facem asta fără să ne expunem la SQL injection și fără să scriem manual tipuri any peste tot?
Capcana SQL Injection și cum te salvează template literals
Cea mai mare greșeală pe care o văd la devii juniori când scriu cod SQL nativ este concatenarea clasică de string-uri. Ceva de genul prisma.$queryRawUnsafe("SELECT * FROM User WHERE id = " + input). Să nu faci asta niciodată în producție.
Prisma folosește tagged templates pentru $queryRaw. Când scrii prisma.$queryRawSELECT * FROM "User" WHERE id = ${userId}``, sub capotă nu se face concatenare de string-uri în motorul de DB. Prisma transmite query-ul separat și valoarea separat ca parametru (prepared statements). Baza de date știe să facă escaping corect. Trade-off-ul? Nu poți folosi parametri dinamici pentru nume de tabele sau coloane în ordonări. Pentru alea chiar ai nevoie de $queryRawUnsafe, dar acolo trebuie să cureți manual intrările.
Type safety la runtime cu Zod
Bun, am rezolvat securitatea prin query-uri parametrizate. Dar cum rămâne cu tipurile? Prisma returnează implicit unknown. Te poți minți singur cu un cast de tipul as MyCustomType[], dar dacă baza de date s-a schimbat subtil și o coloană e acum NULL în loc de NOT NULL, aplicația ta va crăpa silențios la runtime mai încolo.
Aici intră în scenă Zod. În loc să faci aserțiuni oarbe de tip, validezi datele care vin direct din baza de date. E un mic trade-off de performanță (validarea durează câteva milisecunde), dar liniștea pe care o ai știind că datele sunt 100% corecte merită din plin. În plus, poți transforma datele direct la validare (de exemplu, Postgres DECIMAL vine ca string în JS, dar îl poți converti direct în number cu Zod).
Voi cum gestionați query-urile complexe în Prisma? Mergeți pe $queryRaw sau preferați să spargeți logica direct în codul de Node?