import { PrismaClient, Prisma } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const ReportRowSchema = z.object({
accountId: z.string().uuid(),
totalTransactions: z.bigint().transform((val) => Number(val)),
lastActivity: z.date(),
});
export async function getAccountActivity(minCount: number) {
const rawResult = await prisma.$queryRaw<unknown>`
SELECT
a.id AS "accountId",
COUNT(t.id) AS "totalTransactions",
MAX(t."createdAt") AS "lastActivity"
FROM "Account" a
INNER JOIN "Transaction" t ON t."accountId" = a.id
GROUP BY a.id
HAVING COUNT(t.id) >= ${minCount}
`;
return z.array(ReportRowSchema).parse(rawResult);
}Prisma e comod până când ai de scos un raport agregat dintr-un milion de rânduri și vezi că query builder-ul generează un monstru cu subquery-uri inutile. La un proiect de analytics cu vreo 40k tranzacții pe zi, un endpoint dura 1.8 secunde doar din cauza overhead-ului de mapare și join-urilor automate; rescris cu $queryRaw și două window functions, timpul a scăzut la 85ms. Dacă ai ajuns în punctul ăsta, e clar că trebuie să cobori la SQL nativ, dar sunt două riscuri majore: securitatea și minciuna pe care ți-o spui în TypeScript.
Capcana template string-urilor și de ce există $queryRaw
Mulți developeri văd semnul backtick (`) și intră în panică, crezând că e o simplă concatenare de stringuri vulnerabilă la SQL injection. Nu e cazul dacă folosești direct prisma.$queryRaw.
Prisma folosește tagged template literals din ES6. În culise, bucățile de text SQL sunt separate de variabilele tale, iar variabilele sunt trimise către baza de date ca parametri separat (prepared statements). Singurul mod în care te împuști în picior este dacă apelezi la $queryRawUnsafe sau dacă folosești Prisma.raw() cu input venit direct din request fără validare.
Unde crapă des abordarea asta? La clauze dinamice, de exemplu sortarea. Nu poți pasa numele unei coloane ca parametru pregătit în PostgreSQL (ORDER BY $1 nu sortează după coloana aia, ci după valoarea literală). Dacă chiar trebuie să faci sortare dinamică, fă whitelist explicit pe un enum sau pe cheile permise, construind fragmentul de query cu Prisma.sql doar după ce ai validat valoarea.
De ce generic-ul din Prisma te minte la tipuri
Prisma te lasă să scrii prisma.$queryRaw<User[]> și simți că ești în siguranță. Nu ești. Genericul ăla e doar un as User[] mascat. Nu validează absolut nimic.
Cel mai clasic scenariu: PostgreSQL întoarce COUNT(*) sau agregări sumare ca tip BigInt. TypeScript crede că ai un number pentru că așa ai declarat interfața, dar când vrei să serializezi răspunsul către client prin res.json(), Node.js crapă spectaculos cu TypeError: Do not know how to serialize a BigInt. Alt caz comun este la JOIN-uri unde câmpurile din tabelul secundar vin null dacă nu s-a potrivit rândul, deși tipul tău generat din Prisma spunea că proprietatea respectivă e non-nullable.
Soluția curată: parsing runtime cu Zod
Cea mai sănătoasă abordare este să tratezi rezultatul brut ca pe un unknown și să-l treci printr-o schemă Zod. Da, adaugă câteva milisecunde la procesare pentru seturi mari de date, dar trade-off-ul e cinstit: prefer să prind o discrepanță de date în staging cu eroare clară de validare decât să descopăr în producție că am trimis undefined la frontend.
Zod te ajută enorm și la transformări. Poți converti automat BigInt la Number (dacă știi că valoarea încape în limitele sigure de JS) sau poți mapa string-urile returnate de PostgreSQL pentru date calendaristice direct în instanțe de Date.
Coborârea la SQL nativ e adesea soluția corectă pentru performanță, dar nu renunța la controlul tipurilor doar de dragul câtorva zeci de milisecunde economisite. Voi ce abordare aveți când Prisma devine bottleneck: treceți la raw queries sau preferați să scoateți logica complexă în database views?