import { PrismaClient, Prisma } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
// 1. Definim schema Zod pentru validare la runtime
const ProductSearchResultSchema = z.object({
id: z.string().uuid(),
title: z.string(),
price: z.number(),
total_reviews: z.number(),
});
type ProductSearchResult = z.infer<typeof ProductSearchResultSchema>;
async function searchProducts(minPrice: number, limit: number): Promise<ProductSearchResult[]> {
// 2. Query parametrizat în siguranță via tagged template
const rawData = await prisma.$queryRaw<
unknown[]
>`
SELECT
p.id,
p.title,
p.price::float,
COUNT(r.id)::int AS total_reviews
FROM "Product" p
LEFT JOIN "Review" r ON r."productId" = p.id
WHERE p.price >= ${minPrice}
GROUP BY p.id
LIMIT ${limit}
`;
// 3. Validare la runtime - aruncă ZodError dacă baza de date întoarce altceva
return z.array(ProductSearchResultSchema).parse(rawData);
}Am dat de zidul Prisma acum vreo 6 luni, pe un proiect de e-commerce cu vreo 15k produse și căutare faceted. Query-ul generat automat de ORM făcea 4 JOIN-uri inutile și arunca răspunsul în 400ms. Am coborât la $queryRaw, am scris un CTE curat și timpul de răspuns a scăzut direct la 35ms.
Abstractizarea oferită de Prisma e minunată pentru CRUD-uri de zi cu zi. Dar în momentul în care ai nevoie de window functions, subinterogări complexe sau aggregations avansate, ORM-ul devine mai mult un obstacol decât un ajutor. Totuși, când cobori la nivelul de SQL brut, pierzi două mari avantaje: protecția automată împotriva SQL injection și type-safety-ul din TypeScript.
Trapa SQL Injection și cum te salvează Tagged Templates
Cea mai mare greșeală pe care o văd în PR-uri este interpolarea directă a string-urilor. Apelarea metodei $queryRawUnsafe cu un template string clasic este o invitație deschisă la SQL injection. Dacă concatenezi input-ul de la user, ești expus.
Prisma rezolvă asta prin tagged template literals. Când folosești prisma.$queryRaw și îi pasezi variabile directe ${userInput}, Prisma nu face o simplă concatenare. În fundal, transformă variabilele în parametri pregătiți ($1, $2 în Postgres). Păstrezi sintaxa curată și ești protejat automat.
Dacă ai bucăți din interogare care trebuie construite dinamic (de exemplu, o clauză ORDER BY variabilă), NU folosi string-uri simple. Folosește helper-ul Prisma.sql pentru a le izola și a le pasa în siguranță.
De ce as MyType e o minciună și cum te salvează Zod
TypeScript e grozav, dar e complet orb la runtime. Când scrii await prisma.$queryRaw<User[]>..., tu doar îi spui compilatorului "crede-mă pe cuvânt că baza de date returnează structura asta". Dacă redenumiești o coloană în Postgres printr-o migrare manuală și uiți să schimbi interfața din TS, aplicația ta va crăpa tăcut în producție când încearcă să citească un câmp undefined`.
Aici intervine Zod. În loc să faci type casting orb, treci rezultatul brut din $queryRaw printr-o schemă Zod la runtime.
Trade-off-ul e clar: ai un mic overhead de CPU la parsing-ul Zod (neglijabil pentru majoritatea cazurilor) și scrii ceva mai mult cod boilerplate. În plus, pierzi portabilitatea bazei de date — SQL-ul tău brut devine strâns legat de dialectul Postgres sau MySQL. Dar câștigul este uriaș: dacă structura bazei de date se schimbă sau o funcție SQL returnează NULL unde nu te așteptai, aplicația crapă imediat la intrare cu o eroare explicită (ZodError), în loc să propage bug-uri bizare prin 5 servicii mai departe.
Voi cât de des coborâți la $queryRaw în producție sau preferați să lăsați totul în seama ORM-ului?