← Tüm yazılar

Supabase ve Prisma ORM: kurulum, DB-first eşleme ve güvenli sorgular

Prisma 6 ile 7 kurulumunu ayırarak config, pg adapter, TLS, introspection, client üretimi, sorgu ve transaction sınırlarını örneklemek.

t.Turgay
Supabase ve Prisma ORM: kurulum, DB-first eşleme ve güvenli sorgular

Prisma ile Supabase aynı işi yapan iki ürün değil. Supabase burada PostgreSQL’in ve ayrı olarak Storage’ın çalıştığı servis; Prisma ise Node/TypeScript sunucusunun PostgreSQL’e nasıl sorgu göndereceğini belirleyen ORM. İkisini birleştirmek için Supabase verisini tarayıcıya açmak veya SQL şemasını sıfırdan oluşturmak gerekmiyor.

Eski NextPortfolio kaynağımda Prisma 6.15 serisi, prisma-client-js üreticisi, şema içindeki url/directUrl ayarları ve geliştirmede globalThis ile paylaşılan client var. Bu rehber için ayrıca Prisma 7.10.1 ile bağımsız örnek hazırladım; iki sürümün kurulumunu birbirine karıştırmıyorum. Mevcut PortfolioBlog API’si Npgsql/EF Core kullanmayı sürdürüyor. Prisma örneği, aynı yaklaşımın TypeScript tarafında nasıl kurulabileceğini gösteriyor.

Paketleri birlikte ve aynı sürüm ailesinde kurmak

npm init -y
npm pkg set type=module
npm install @prisma/client@7.10.1 @prisma/adapter-pg@7.10.1 pg dotenv
npm install --save-dev prisma@7.10.1 typescript tsx @types/node @types/pg

Örneği Node 24 ile kurdum. Prisma CLI, @prisma/client ve @prisma/adapter-pg aynı 7.10.1 sürümüne sabitli. pg veritabanı sürücüsü, adapter onun Prisma ile bağlantısı, Client üretilmiş sorgu API’si. dotenv ortam dosyasını, tsx TypeScript giriş dosyasını çalıştırıyor. Bir paketi rastgele yeni major sürüme geçirip diğerlerini eski bırakmak burada izlediğim yol değil.

Kod blokları, bu yazının kurulum adımlarını izleyerek oluşturacağın bağımsız bir Prisma projesi içindir. node_modules ve üretilmiş client Git’e girmez; package-lock.json tekrarlanabilir kurulum için saklanır. Örneğin paketleri ana Angular veya API çalışma zamanına eklenmiyor. Bir makale için kurulan araç, fark edilmeden üretim bağımlılığına dönüşmemeli.

Runtime bağlantısı ile CLI bağlantısını ayırmak

# Runtime: verified TLS is configured explicitly in the pg adapter.
DATABASE_URL="postgresql://postgres.YOUR_PROJECT_REF:YOUR_PASSWORD@YOUR_POOLER_HOST:5432/postgres"
PG_CA_PATH="/absolute/path/to/provider-ca.crt"
# CLI: its own connection and certificate options, not the pg adapter settings.
DIRECT_URL="postgresql://postgres.YOUR_PROJECT_REF:YOUR_PASSWORD@YOUR_POOLER_HOST:5432/postgres?sslmode=require&sslcert=YOUR_URL_ENCODED_ABSOLUTE_CA_PATH&sslaccept=strict"

DATABASE_URL uygulama sorgularına, DIRECT_URL CLI/introspection işlemlerine ait. Bu örnekte ikisi de IPv4 uyumlu Session pooler/5432 kullanıyor; DIRECT_URL adı mutlaka IPv6 direct host kullanılıyor demek değil. Host ve postgres.PROJECT_REF kullanıcı adı panelden alınır. Şifre ve CA yolu URI içine yazılırken özel karakterler encode edilir. Gösterilenler gerçek proje bilgileri değil.

Runtime TLS doğrulamasını pg adapter’ının ssl nesnesinde yapıyorum. Bu yüzden DATABASE_URL içine sslmode/sslcert/sslkey/sslrootcert eklenmesini kod reddediyor: node-postgres bu URI seçenekleriyle açık ssl nesnesini değiştirebilir. CLI aynı adapter’ı çalıştırmadığı için onun DIRECT_URL değerinde sslmode=require, sağlayıcının public CA yolu olan sslcert ve sslaccept=strict ayrıca tanımlı. İki bağlantı yolunu gerçekten ayrı denetlemek gerekiyor.

import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  schema: 'prisma/schema.prisma',
  datasource: { url: env('DIRECT_URL') },
});

Prisma 7’de CLI bağlantısını prisma.config.ts belirler. Şema dosyası verinin şeklini, config ise CLI’nin bağlantısını taşır. env("DIRECT_URL") eksik ayarı erken fark ettirir. Bu örnekte validate/generate de config yüklediği için değişken tanımlı olmalı; yalnız tip üretimi için geçerli biçimli bir geliştirme yer tutucusu yeterli olabilir. Bağlantı kuracak db pull için ise gerçek ve yetkili sunucu ayarı gerekir.

Var olan tabloya isim eşlemek, tabloyu yeniden yaratmak değildir

generator client {
  provider = "prisma-client"
  output   = "../generated"
}

datasource db {
  provider = "postgresql"
}

// Read projection of an existing table, not the table's complete write model.
// Do not run Prisma Migrate or db push against the PortfolioBlog schema.
model Post {
  id              String   @id @db.Uuid
  slug            String   @unique @db.VarChar(150)
  titleTr         String   @map("title_tr") @db.VarChar(200)
  published       Boolean
  deleted         Boolean
  sourceCreatedAt DateTime @map("source_created_at") @db.Timestamptz(6)

  @@map("post")
}

Model Post, PostgreSQL’deki post tablosuna @@map ile; titleTr, title_tr kolonuna @map ile bağlanır. Böylece TypeScript’te okunabilir alan adı kullanırken SQL kolonunu yeniden adlandırmak gerekmiyor. sourceCreatedAt gerçek timestamptz(6) kolonunu izliyor. String yazmak bütün kolonların PostgreSQL tarafında aynı uzunluk/tür/kısıta sahip olduğu anlamına gelmez.

Bu şema bilinçli olarak yalnız okunacak alanları gösteriyor. PortfolioBlog’un category_id, site_id, iki dilli gövde, sıra ve diğer zorunlu alanları burada yok. Dolayısıyla bunu tam yazma modeli sayıp create çağrısı eklemiyorum. Aynı nedenle bu dosyadan canlı şemaya migrate veya db push çalıştırmak mevcut kolonları/ilişkileri yanlış yorumlayabilir. SQL’le yönetilen uygulama şeması kendi otoritesini koruyor.

Introspection, generate ve migration komutlarını ayırmak

npx prisma db pull --print
npx prisma validate
npx prisma generate
npx tsc --noEmit

db pull --print, gerçek veritabanını okuyup modeli stdout’a verir; bu örnekteki dosyayı değiştirmez. --print olmadan db pull yerel schema.prisma dosyasını günceller; yine de tabloyu değiştirmez. generate ise şemadan TypeScript client üretir, veritabanına migration uygulamaz. SQL-first akışta önce kontrollü SQL değişir, sonra model incelenip client yeniden üretilir.

Yeni ve ayrı bir uygulamada migration yönetimini Prisma’ya vermek ayrıca seçilebilir. O durumda migrate dev geliştirme, incelenmiş migration’ların migrate deploy ile uygulanması dağıtım akışının parçasıdır. Fakat bu rehberin kısmi okuma şemasıyla PortfolioBlog üzerinde migrate reset, db push veya migrate dev çalıştırmıyorum. Bir ORM’ye bağlanmak, bütün veritabanının şema sahipliğini ona devretmek zorunda olduğum anlamına gelmiyor.

pg adapter’ı: havuz ve doğrulanan sertifika

import 'dotenv/config';
import fs from 'node:fs';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/client.ts';

const connectionString = process.env.DATABASE_URL;
const certificatePath = process.env.PG_CA_PATH;
if (!connectionString || !certificatePath) throw new Error('Set DATABASE_URL and PG_CA_PATH.');
const url = new URL(connectionString);
// pg's URI sslmode/sslrootcert options override the explicit ssl object.
if (['sslmode', 'sslcert', 'sslkey', 'sslrootcert'].some((key) => url.searchParams.has(key)))
  throw new Error('Remove URI SSL options; verified TLS is configured by the adapter.');
if (url.port === '6543') throw new Error('This example uses Session pooler port 5432.');

export function createDatabase() {
  const adapter = new PrismaPg({
    connectionString,
    max: 5,
    connectionTimeoutMillis: 15000,
    ssl: { rejectUnauthorized: true, ca: fs.readFileSync(certificatePath!, 'utf8') },
  });
  return new PrismaClient({ adapter });
}

createDatabase bir factory. Sürekli çalışan sunucu bu factory’yi her istekte çağırmamalı; uygun süreç ömründe bir client kullanmalı. max=5 tek pg havuzunun bütçesi, connectionTimeoutMillis bağlantı beklemesinin sınırı. CA içeriği dosyadan okunur ve rejectUnauthorized=true kalır. Kod örneğinin Session/5432 tercihi kendi sınırıdır; serverless dağıtımda farklı pooling ihtiyacı ayrıca değerlendirilir.

İhtiyaç kadar kolon ve kararlı sırayla okumak

import { createDatabase } from './db.ts';

const prisma = createDatabase();
try {
  const posts = await prisma.post.findMany({
    where: { published: true, deleted: false },
    select: { slug: true, titleTr: true, sourceCreatedAt: true },
    orderBy: [{ sourceCreatedAt: 'desc' }, { id: 'asc' }],
    take: 5,
  });
  console.log(JSON.stringify(posts, null, 2));
} finally {
  await prisma.$disconnect();
}

where yayın ve silinme durumunu SQL’de filtreler. select yalnız kart için gereken alanları alır. Aynı tarihli kayıtlar id ile sıralanır; take=5 ilk beş sonucu sınırlar. Prisma’nın ürettiği sorgu API’si parametreli erişim sağlar; ham SQL gerekiyorsa kullanıcı girdisini string birleştirmeyle sorguya sokmam. TypeScript tip güvenliği ise HTTP isteğinin gerçekten güvenilir veri olduğunu kanıtlamaz; giriş doğrulaması hâlâ gerekir.

Bu giriş dosyası tek iş yapan CLI olduğu için finally içinde $disconnect çağırıyor. Web endpoint’inde her isteğin sonunda bağlantıyı kapatmak yerine süreçte ortak client kullanılır. Geliştirmede hot reload örnek sayısını artırmasın diye NextPortfolio’da da bulunan yaklaşım aşağıdaki gibi uygulanabilir; modül yalnız sunucu tarafında kalır.

// Server-only module for a persistent development server.
// createDatabase is the factory shown above; never import this into a client bundle.
import { createDatabase } from './db';

type Client = ReturnType<typeof createDatabase>;
const shared = globalThis as typeof globalThis & { portfolioPrisma?: Client };
export const prisma = shared.portfolioPrisma ?? createDatabase();
if (process.env.NODE_ENV !== 'production') shared.portfolioPrisma = prisma;

Güncelleme örneği auth ve transaction yerine geçmez

// Server-side extension for an authorized administrative operation.
// It is NOT executed by the read-only example.
const changed = await prisma.post.updateMany({
  where: { id: authorizedPostId, deleted: false },
  data: { published: false }
});
if (changed.count !== 1) throw new Error('Record missing or state changed.');

Bu kısa uzantı yalnız önceden yetkilendirilmiş bir admin işlemini anlatır; örneğin read komutu bunu çalıştırmaz. authorizedPostId istemciden geldi diye otomatik yetkili sayılmaz. updateMany sonucu count ile kontrol edilir; kayıt artık yoksa veya durumu değiştiyse sessiz başarı verilmez. PortfolioBlog’un revizyon artırma ve Serializable kaydetme akışını bu tek çağrı taklit etmiyor.

Birden fazla bağımlı değişiklikte $transaction sınırı, isolation ve çakışma davranışı ayrıca tanımlanır. transaction içine Storage HTTP çağrısını koymak onu PostgreSQL’le atomik yapmaz. Sahip/ayrıcalıklı SQL rolüyle bağlanan Prisma client’ın Supabase RLS kurallarından otomatik kullanıcı kimliği çıkaracağını da varsaymıyorum. Public sorgu filtresi, API auth ve rol yetkileri birlikte düşünülür.

Kontroller ve sınırlar

  • Prisma 7.10.1 şeması doğrulandı; client üretildi ve tsc --noEmit geçti.
  • Örnek, doğrulanan TLS ile Supabase’den beş kayıt okudu. db pull --print de CA/strict kabul ayarlarıyla salt okunur çalıştırıldı.
  • Bu kontroller yazma migration’ı, performans testi veya tüm NextPortfolio’nun Prisma 7’ye yükseltildiği iddiası değil.
  • Storage SDK ve service_role anahtarı ORM sorgusu için gerekmez; SQL bağlantısı yeterlidir. Client ve SQL parolası tarayıcıya dağıtılmaz.

Prisma’yı eklerken en yararlı ayrım, üretilen client’ın hangi şemaya ve hangi bağlantı yoluna dayandığının açık olması. Sorgu yazmayı kolaylaştırması, yayın filtresi, transaction veya yetkilendirme kararlarını benim yerime verdiği anlamına gelmiyor.

Başvurular

✳ Bu yörüngenin sonuna geldin.
Diğer yazıları keşfet →
turgay.Biraz kod, biraz piksel, bolca merak.© 2026 · Dünya’dan sevgilerle ✳