eduardweb.
Prisma ORMAvansat#typescript#prisma#postgresql#backend#zod

Prisma $queryRaw la producție: securitate, performanță și type safety cu Zod

De Delia Petre, 8 oct. 2026 · 12 vizualizări · 2 like-uri

Postat 8 oct. 2026
typescript
import { PrismaClient, Prisma } from '@prisma/client';
import { z } from 'zod';

const prisma = new PrismaClient();

const ReportRowSchema = z.object({
  userId: z.string().uuid(),
  totalSpent: z.coerce.number(),
  ordersCount: z.coerce.number(),
  lastOrderDate: z.date().nullable(),
});

type ReportRow = z.infer<typeof ReportRowSchema>;

export async function getTopSpenders(limit: number, minOrders: number): Promise<ReportRow[]> {
  // Tagged template = parametrizează automat limit și minOrders
  const rawData = await prisma.$queryRaw<unknown>`
    SELECT 
      u.id AS "userId",
      COALESCE(SUM(o.total), 0) AS "totalSpent",
      COUNT(o.id) AS "ordersCount",
      MAX(o.created_at) AS "lastOrderDate"
    FROM users u
    LEFT JOIN orders o ON o.user_id = u.id
    GROUP BY u.id
    HAVING COUNT(o.id) >= ${minOrders}
    ORDER BY "totalSpent" DESC
    LIMIT ${limit};
  `;

  // Parsing-ul runtime garantează că datele respectă contractul
  return z.array(ReportRowSchema).parse(rawData);
}

Prisma e grozav pentru 80% din CRUD-ul zilnic, dar când dai de rapoarte grele sau agregări complexe, abstractizarea te îngroapă. La o aplicație e-commerce cu vreo 450k comenzi în baza de date, un dashboard care folosea findMany cu 4 niveluri de relații dura 4.2 secunde și mânca 300MB RAM în Node.js; l-am rescris cu un CTE în $queryRaw și a coborât instant la 65ms.

Dar coborârea în SQL brut vine la pachet cu două riscuri mari: găuri de securitate și minciuna de la compile-time că datele tale sunt sigure.

Când merită să cobori din ORM

Nu folosi $queryRaw doar pentru că ți-e lene să scrii un where complex în sintaxa Prisma. Faci switch-ul când ai nevoie de facilități specifice motorului de baze de date pe care Prisma nu le suportă nativ: window functions (ROW_NUMBER(), RANK()), agregări avansate de JSON în Postgres (jsonb_agg), CTE-uri recursive sau DISTINCT ON.

Trade-off-ul e direct: pierzi validarea automată a tipurilor generată de prisma generate, nu mai ai redenumiri automate când schimbi schema și trebuie să gestionezi manual diferențele de tipuri ciudate, cum ar fi BigInt din Postgres care devine string sau dă eroare la JSON.stringify.

Iluzia de securitate și capcana Prisma.raw()

Mulți colegi juniori cred că orice prisma.$queryRaw te protejează automat de SQL injection. Adevărul e la jumătate: Prisma folosește tagged template literals. Când scrii codul așa:

await prisma.$queryRaw`SELECT * FROM users WHERE email = ${userEmail}`

Prisma trimite interogarea parametrizată ($1, $2), deci ești în deplină siguranță. Problema apare când ai nevoie de bucăți dinamice de query, cum ar fi direcția de sortare (ASC/DESC) sau numele unei coloane. Numele de coloane nu pot fi parametrizate în SQL.

În acel moment, tentația este să apelezi la Prisma.raw(userInput). Dacă ai concatenat direct un input neverificat venit din request în Prisma.raw(), ai deschis ușa larg oricărui atacator. Soluția sănătoasă este un whitelist rigid: verifici inputul împotriva unui set fix de coloane înainte de a-l introduce în query.

Type-safety pe bune: de ce Prisma generic nu e de ajuns

Când rulezi prisma.$queryRaw<UserReport[]>, acel <UserReport> este doar o dorință la nivel de TypeScript. Dacă în baza de date o coloană amount devine NUMERIC(10,2) și driverul ți-o întoarce ca string, sau dacă un LEFT JOIN produce un null neașteptat, TypeScript nu te va avertiza cu nimic. Te trezești cu erori ciudate direct în producție.

Aici intră Zod. În loc să mă bazez pe generics orbești, tratez rezultatul ca unknown și îl trec printr-o schemă Zod. Dacă baza de date întoarce date corupte sau o structură schimbată, aplicația aruncă o eroare clară la frontieră, înainte ca datele să ajungă în logica de business.

Câștigi predictibilitate absolută cu prețul câtorva milisecunde de parsing.

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.