import { prisma } from './db';
import { z } from 'zod';
const UserReportSchema = z.object({
id: z.number(),
email: z.string().email(),
total_spent: z.number().transform(val => Number(val)),
});
type UserReport = z.infer<typeof UserReportSchema>;
async function getHighSpenders(minAmount: number): Promise<UserReport[]> {
// Safe from SQL injection thanks to tagged template literal
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}
LIMIT 100;
`;
// Safe runtime validation and conversion
return z.array(UserReportSchema).parse(rawData);
}Am iubit Prisma de la prima schemă, dar dragostea s-a cam stins când am avut de făcut un raport cu 4 JOIN-uri și agregări complexe pentru 12.000 de clienți activi. Query-ul generat de ORM avea vreo trei pagini de SQL autogenerat și rula în mai bine de 4 secunde. Atunci am decis să cobor în mină, la $queryRaw, și am scos aceleași date în doar 85 de milisecunde.
Totuși, când renunți la căldura ORM-ului, renunți și la plasa de siguranță. Dacă nu ești atent, deschizi ușa larg pentru SQL injection și pierzi complet certitudinea tipurilor de date la runtime.
Capcana stringurilor interpolate
Mulți juniori cred că dacă folosesc $queryRaw, Prisma îi protejează automat de SQL injection. Parțial adevărat, dar doar dacă folosești tagged templates corect.
Sub capotă, Prisma folosește pachetul sql-template-tag. Când scrii un query direct cu backticks, valorile interpolate sunt trimise ca parametri securizați (prepared statements) către Postgres.
Problema apare când încerci să fii "inteligent" și construiești stringul SQL înainte, folosind concatenare clasică sau string interpolation manual în afara metodei Prisma. Am văzut cod în producție unde cineva făcea const query = 'SELECT * FROM User WHERE id = ' + id; și apoi rula prisma.$queryRawUnsafe(query). Este rețeta perfectă pentru un dezastru când un utilizator rău intenționat trimite '1 OR 1=1' în request. Regula mea e simplă: nu atinge $queryRawUnsafe decât dacă scrii un tool de migrare sau ai de trecut dinamic nume de tabele, caz în care oricum trebuie să sanitizezi manual ca la carte.
Iluzia de type-safety din Prisma
Când scrii await prisma.$queryRaw<User[]>(...), ai impresia că ești protejat de TypeScript. Nu ești. Genericul ăla e doar un type assertion chior, un "trust me, bro" mascat. Dacă schimbi o coloană în baza de date fără să actualizezi manual tipul ăla definit de tine, TypeScript nu va zice nimic la build, dar codul tău va crăpa spectaculos în producție.
Aici intervine Zod. Da, adaugă un mic overhead la runtime (am măsurat cam 8ms în plus la un set de 5.000 de rânduri parseate), dar prefer să pierd acele milisecunde decât să mă sune clientul duminica că raportul lui e gol sau dă erori de tip cannot read property of undefined.
Cum legăm totul cap la cap
Soluția pe care o folosesc acum combină tagged templates pentru securitate și Zod pentru validare la runtime. Definesc o schemă strânsă pentru ce returnează query-ul și parsez rezultatul imediat ce vine din baza de date. Dacă baza de date îmi trimite un string în loc de un BigInt pe care îl așteptam, schema Zod se ocupă de conversie sau aruncă o eroare explicită imediat în testele de integrare.
Această abordare merge brici pentru query-uri de citire complexe și rapoarte unde viteza e critică. Totuși, trade-off-ul e că pierzi flexibilitatea de a folosi middleware-urile de Prisma și trebuie să îți scrii singur maparea pentru tipurile custom de Postgres.
Voi cum gestionați rapoartele grele în Prisma? Rămâneți pe ORM până la capăt sau coborâți și voi la SQL chior când lucrurile devin serioase?