import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const TransactionReportSchema = z.object({
id: z.string().uuid(),
amount: z.number(),
createdAt: z.coerce.date(),
userEmail: z.string().email(),
});
type TransactionReport = z.infer<typeof TransactionReportSchema>;
async function getHighValueTransactions(minAmount: number): Promise<TransactionReport[]> {
// Tagged template-ul protejează împotriva SQL injection pentru parametrii standard
const rawData = await prisma.$queryRaw`
SELECT t.id, t.amount, t."createdAt", u.email as "userEmail"
FROM "Transaction" t
JOIN "User" u ON t."userId" = u.id
WHERE t.amount > ${minAmount}
LIMIT 100
`;
// Validăm datele la runtime și obținem tipuri TS sigure
return z.array(TransactionReportSchema).parse(rawData);
}Prisma e minunată pentru CRUD-ul de zi cu zi. Generarea automată de tipuri ne face viața ușoară, dar ORM-ul își arată limitele când ai de-a face cu interogări complexe. Am avut un proiect recent cu peste 8 milioane de rânduri în tabela de tranzacții, unde un simplu query de raportare generat de Prisma făcea niște join-uri atât de proaste încât baza de date dădea timeout după 30 de secunde. Soluția a fost să cobor la SQL brut folosind $queryRaw, scăzând timpul de răspuns la doar 120ms. Dar trecerea asta vine cu riscuri mari dacă nu ești atent.
Iluzia de siguranță din $queryRaw
Mulți dev-i cred că dacă folosesc $queryRaw, Prisma se ocupă automat de tot ce înseamnă securitate. Este parțial adevărat, dar doar dacă folosești tagged templates corect.
Când scrii prisma.$queryRawSELECT * FROM "User" WHERE id = ${userId}``, Prisma nu face o simplă concatenare de stringuri. Sub capotă, template literal-ul este trimis către baza de date ca o interogare parametrizată (cu $1, $2 în Postgres). Asta te protejează complet de SQL injection.
Problema apare când ai nevoie de parametri dinamici care nu pot fi trimiși ca placeholderi SQL standard, cum ar fi numele coloanelor pentru sortare sau clauze dinamice de ORDER BY. SQL-ul nativ nu acceptă parametrizați acolo. În acel moment, mulți cedează tentației și trec la $queryRawUnsafe, concatenând direct inputul utilizatorului în string. Este cel mai rapid mod de a-ți lăsa baza de date complet expusă.
Cum rezolvăm sortarea dinamică în siguranță
Dacă ai nevoie de sortare dinamică, regula de aur este să folosești un whitelist strict în codul tău TypeScript. Nu lăsa niciodată baza de date să primească direct ce vine din request.
Definesc o listă de coloane permise și direcții de sortare acceptate direct în cod. Dacă inputul nu se potrivește exact, folosesc o valoare de fallback sigură. Doar după această validare manuală concatenăm stringul, fiind 100% siguri că nicio intrare malițioasă nu poate ajunge în query.
Recuperarea tipurilor statice cu ajutorul Zod
Cea mai mare problemă când treci pe raw SQL în Prisma este că pierzi complet siguranța tipurilor. Prisma îți va returna tipul unknown sau any. Poți folosi type assertion ca as MyType[], dar asta doar păcălește compilatorul de TypeScript la build. Dacă baza de date returnează altceva la runtime (de exemplu, un câmp null pe care îl credeai obligatoriu), aplicația va crăpa silențios mai târziu în cod.
Aici intervine Zod. În loc să mințim compilatorul cu type casting, mai bine validăm datele imediat ce vin din baza de date. Definim un schemă Zod care reflectă exact structura pe care o așteptăm de la query-ul brut.
Trade-off-ul este un mic overhead de performanță la runtime pentru parsarea obiectelor, dar pentru 95% din cazuri, siguranța că datele tale sunt exact în formatul așteptat merită din plin cele câteva milisecunde pierdute.
Tu cum gestionezi interogările complexe în Prisma? Preferi să scrii SQL brut sau încerci să forțezi API-ul lor până când obții ce vrei?