İçeriğe geç
academia.sh

Ders 34 / 34

HTTP Üzerinden GraphQL

Tek uç nokta üzerinden aktarım, POST ile GET arasındaki önbellekleme farkı, önceden kaydedilmiş belgeler ve tek adresin aracılara ve kütüklere bıraktığı bilginin ölçülmesi.

İçindekiler

Buraya kadar yazılan her şey tek bir süreç içinde çalıştı: sorgu bir dizgiydi, sonuç bir nesne. Gerçek bir servis sorguyu ağ üzerinden alır ve yanıtı ağ üzerinden döndürür.

Bu yalnız bir sarmalama işi değildir. Tek uç nokta ve çoğunlukla tek yöntem kullanan bir aktarım, önceki konuda kurulan her şeyi yeniden düşünmeyi gerektirir: adres tabanlı önbellekleme, durum kodu eşlemesi, aracıların ve kütüklerin gördüğü bilgi. Bu ders çalıştırıcıyı HTTP’ye oturtur ve bu kararın ne götürüp ne getirdiğini ölçer.

Sunucu

Sunucu üç aktarım biçimini destekler. POST gövdesinde sorguyu, değişkenleri ve işlem adını taşır. GET aynı üçlüyü sorgu parametrelerinde taşır. Üçüncüsü, sorgu metni yerine önceden kaydedilmiş belgenin kimliğini taşır.

Önceki derslerde yazılan ayrıştırıcı, denetleyici, şema, maliyet ölçeri ve çalıştırıcı olduğu gibi kullanılır; sunucu bunları sırayla çağıran ince bir katmandır.

// sunucu.mjs — GraphQL'i HTTP uzerine oturtur
//   POST /graphql            govdede { query, variables, operationName }
//   GET  /graphql?query=...  sorgu adreste; onbelleklenebilir
//   GET  /graphql?kimlik=... onceden kaydedilmis belge; kisa ve izin listesiyle sinirli
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { ayristir } from "./ayristirici.mjs";
import { denetle } from "./denetle.mjs";
import { calistir } from "./calistirici.mjs";
import { olc, sinirDenetle } from "./maliyet.mjs";
import { SEMA_L as SEMA } from "./sema-liste.mjs";
import { COZUCULER } from "./veri.mjs";

const SINIR = { enCokDerinlik: 6, enCokMaliyet: 1000 };
export const kimlikYaz = (metin) => createHash("sha256").update(metin).digest("hex").slice(0, 12);

// Onceden kaydedilmis belgeler: istemci yalniz kimligini gonderir.
export const KAYITLI = new Map([
  `query OduncListesi { oduncler(ilk: 6) { id durum } }`,
  `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`,
].map((m) => [kimlikYaz(m), m]));

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

const yaz = (yanit, kod, nesne, ekBaslik = {}) => {
  yanit.writeHead(kod, { "content-type": "application/graphql-response+json; charset=utf-8", ...ekBaslik });
  yanit.end(JSON.stringify(nesne));
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname !== "/graphql") return yaz(yanit, 404, { errors: [{ ileti: "yol yok", kod: "yol_yok" }] });

  let metin, degiskenler = {}, islemAdi;
  if (istek.method === "POST") {
    const g = JSON.parse((await govdeOku(istek)) || "{}");
    ({ query: metin, variables: degiskenler = {}, operationName: islemAdi } = g);
  } else {
    const kimlik = adres.searchParams.get("kimlik");
    metin = kimlik ? KAYITLI.get(kimlik) : adres.searchParams.get("query");
    degiskenler = JSON.parse(adres.searchParams.get("variables") ?? "{}");
    islemAdi = adres.searchParams.get("operationName") ?? undefined;
    if (kimlik && !metin) return yaz(yanit, 404, { errors: [{ ileti: "kayıtlı belge yok", kod: "belge_yok" }] });
  }

  let belge;
  try { belge = ayristir(metin ?? ""); }
  catch (h) { return yaz(yanit, 400, { errors: [{ ileti: h.message, kod: "ayristirma" }] }); }

  const denetimHatalari = denetle(SEMA, belge);
  if (denetimHatalari.length) return yaz(yanit, 400, { errors: denetimHatalari.map((i) => ({ ileti: i, kod: "denetim" })) });

  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const olcum = olc(SEMA, belge, islem, { varsayilanCarpan: 6, degiskenler });
  const asim = sinirDenetle(olcum, SINIR);
  if (asim.length) return yaz(yanit, 400, { errors: asim.map((i) => ({ ileti: i, kod: "sinir" })) });

  const sonuc = await calistir(SEMA, COZUCULER, belge, { islemAdi, degiskenler });
  // Alan duzeyi hata kismi basaridir: istek yerine getirilmistir, 200 doner.
  const baslik = istek.method === "GET" ? { "cache-control": "public, max-age=30" } : { "cache-control": "no-store" };
  yaz(yanit, 200, sonuc, { ...baslik, "graphql-islem": islem.ad ?? "adsiz" });
}).listen(8441, "127.0.0.1", () => {
  console.log("graphql sunucusu 127.0.0.1:8441");
  for (const [k, m] of KAYITLI) console.log(`kayitli ${k} ${/query (\w+)/.exec(m)[1]}`);
});
#!/usr/bin/env bash
# Uc aktarim bicimi ve iki hata durumu
node sunucu.mjs > /tmp/baslangic & s=$!
sleep 0.6
cat /tmp/baslangic
KIMLIK=$(grep 'OduncAyrinti' /tmp/baslangic | cut -d' ' -f2)

goster() { printf -- '--- %s ---\n' "$1"; shift; curl -sS -D - -o /tmp/g "$@" | grep -iE '^HTTP|^cache-control|^graphql-islem' | tr -d '\r'; cat /tmp/g; echo; }

goster "POST (gövdede sorgu)" -X POST -H 'content-type: application/json' \
  -d '{"query":"query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }","variables":{"id":"O-2"}}' \
  http://127.0.0.1:8441/graphql

goster "GET (adreste sorgu)" --get --data-urlencode 'query=query OduncListesi { oduncler(ilk: 2) { id durum } }' \
  http://127.0.0.1:8441/graphql

goster "GET (kayıtlı belge)" "http://127.0.0.1:8441/graphql?kimlik=$KIMLIK&variables=%7B%22id%22%3A%22O-1%22%7D"

goster "şemada olmayan alan" -X POST -H 'content-type: application/json' \
  -d '{"query":"{ odunc(id: \"O-1\") { gecikmeCezasi } }"}' http://127.0.0.1:8441/graphql

goster "sınır aşımı" -X POST -H 'content-type: application/json' \
  -d '{"query":"{ oduncler(ilk: 500) { id uye { ad oduncler(ilk: 500) { id } } } }"}' http://127.0.0.1:8441/graphql

kill "$s"; wait "$s" 2>/dev/null
graphql sunucusu 127.0.0.1:8441
kayitli e51e709a4e07 OduncListesi
kayitli dcb9c7bb85fa OduncAyrinti
--- POST (gövdede sorgu) ---
HTTP/1.1 200 OK
cache-control: no-store
graphql-islem: OduncAyrinti
{"data":{"odunc":{"id":"O-2","durum":"ACIK","uye":{"ad":"Üye 1"}}}}
--- GET (adreste sorgu) ---
HTTP/1.1 200 OK
cache-control: public, max-age=30
graphql-islem: OduncListesi
{"data":{"oduncler":[{"id":"O-1","durum":"ACIK"},{"id":"O-2","durum":"ACIK"}]}}
--- GET (kayıtlı belge) ---
HTTP/1.1 200 OK
cache-control: public, max-age=30
graphql-islem: OduncAyrinti
{"data":{"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Üye 0"}}}}
--- şemada olmayan alan ---
HTTP/1.1 400 Bad Request
{"errors":[{"ileti":"Sorgu.odunc: Odunc tipinde \"gecikmeCezasi\" alanı yok","kod":"denetim"}]}
--- sınır aşımı ---
HTTP/1.1 400 Bad Request
{"errors":[{"ileti":"maliyet 502000, sınır 1000","kod":"sinir"}]}

Belge kimlikleri karma işlevinden türetildiği için makineden makineye değişmez, ama belge metni değiştiğinde kimlik de değişir.

Durum Kodunun Anlamı

Yanıtların durum kodları iki gruba ayrılıyor ve ayrım keyfî değildir.

400 dönen iki durum, sorgunun hiç çalıştırılmadığı durumlardır: belge ayrıştırılamamış, şemaya uymuyor ya da sınırı aşıyor. İstek yerine getirilmemiştir; istemcinin gönderdiği şey hatalıdır ve yeniden denemek işe yaramaz.

200 dönen durumlar, sorgunun çalıştırıldığı durumlardır — alan düzeyinde hata bulunsa bile. Önceki derste kurulan kısmi başarı budur: bir alanın çökmesi isteğin yerine getirilmediği anlamına gelmez, çünkü yanıtın geri kalanı geçerlidir. Yanıtta hem data hem errors bulunabilir ve durum kodu bunu bildiremez.

Ayrım, önceki konudaki 400 ile 422 ayrımının aynısıdır: anlaşılamayan istek ile anlaşılıp kısmen karşılanan istek farklı şeylerdir. Buradaki fark, ikinci grubun başarısız sayılmamasıdır.

Aracıların ve Kütüklerin Gördüğü

Tek uç nokta kararının asıl bedeli, isteğin ayırt edici bilgisinin adresten gövdeye taşınmasıdır. Yol üzerindeki bir önbellek ya da erişim kütüğü isteği yalnız yöntem ve adresten tanır.

// aracinin-gordugu.mjs — yol uzerindeki bir araci istegi yalnizca yontem+adresten tanir.
// Ayni is yuku dort aktarim bicimiyle yollanirsa aracinin gordugu ne kadar degisir?
import { createHash } from "node:crypto";
const kimlikYaz = (m) => createHash("sha256").update(m).digest("hex").slice(0, 12);

// Yarim gunluk is yuku: dort ayri islem, tekrarli
const ISLEMLER = [
  { ad: "OduncListesi", yol: "/oduncler", sorgu: `query OduncListesi { oduncler(ilk: 6) { id durum } }`, degisken: {} },
  { ad: "OduncAyrinti", yol: "/odunc/O-1", sorgu: `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: { id: "O-1" } },
  { ad: "OduncAyrinti", yol: "/odunc/O-2", sorgu: `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: { id: "O-2" } },
  { ad: "UyeOzeti", yol: "/uyeler/U-1001", sorgu: `query UyeOzeti($k: ID!) { uye(kimlik: $k) { ad ceza } }`, degisken: { k: "U-1001" } },
];
const YUK = Array.from({ length: 12 }, (_, i) => ISLEMLER[i % ISLEMLER.length]);

const enc = encodeURIComponent;
const BICEMLER = {
  "kaynak temelli (GET)":   (o) => ["GET", o.yol],
  "GraphQL POST":           () => ["POST", "/graphql"],
  "GraphQL GET (sorgu)":    (o) => ["GET", `/graphql?query=${enc(o.sorgu)}&variables=${enc(JSON.stringify(o.degisken))}`],
  "GraphQL GET (kayıtlı)":  (o) => ["GET", `/graphql?kimlik=${kimlikYaz(o.sorgu)}&variables=${enc(JSON.stringify(o.degisken))}`],
};

console.log("aktarım                 istek  önbellek anahtarı  önbellekten  ayırt edilen işlem  ort. adres");
for (const [ad, uret] of Object.entries(BICEMLER)) {
  const anahtarlar = YUK.map((o) => uret(o).join(" "));
  const tekil = new Set(anahtarlar);
  // Araci yalniz GET isteklerini onbellekleyebilir.
  const onbelleklenebilir = anahtarlar[0].startsWith("GET") ? YUK.length - tekil.size : 0;
  const islemAyrimi = new Set(anahtarlar.map((a) => a.split("?")[0] + (a.includes("query=") || a.includes("kimlik=") ? a.split("&")[0].split("?")[1] : "")));
  const ortUzunluk = Math.round(anahtarlar.reduce((t, a) => t + a.length, 0) / anahtarlar.length);
  console.log(`${ad.padEnd(23)} ${String(YUK.length).padStart(5)}  ${String(tekil.size).padStart(17)}  ${String(onbelleklenebilir).padStart(11)}  ${String(islemAyrimi.size).padStart(18)}  ${String(ortUzunluk).padStart(10)}`);
}
aktarım                 istek  önbellek anahtarı  önbellekten  ayırt edilen işlem  ort. adres
kaynak temelli (GET)       12                  4            8                   4          15
GraphQL POST               12                  1            0                   1          13
GraphQL GET (sorgu)        12                  4            8                   3         154
GraphQL GET (kayıtlı)      12                  4            8                   3          65

POST satırı tek uç noktanın bedelini gösteriyor: on iki isteğin tamamı tek bir önbellek anahtarına düşüyor, hiçbiri önbellekten karşılanamıyor ve erişim kütüğü on iki isteği tek bir satır türü olarak görüyor. Bir aracı açısından bütün istekler aynıdır.

GET satırları bunu geri kazanıyor. Sorgu adrese taşındığında önbellek anahtarı sayısı kaynak temelli aktarımla eşitleniyor ve on iki isteğin sekizi önbellekten karşılanabilir hâle geliyor. Bedeli adres uzunluğudur: ortalama 154 karakter, ve gerçek sorgular bundan çok daha uzundur. Kayıtlı belgeler bu bedeli 65 karaktere indiriyor.

Ayırt edilen işlem sütunundaki 4’e karşı 3 farkı bir eksiklik değil, farklı bir gruplamadır. Kaynak temelli kütük istekleri kaynağa göre ayırır; GraphQL kütüğü işleme göre. OduncAyrinti iki farklı kimlikle çağrıldığında ikinci gruplamada tek satırdır. Hangisinin işe yaradığı sorulan soruya bağlıdır: “hangi kaynak çok okunuyor” sorusu birinciyi, “hangi ekran yavaş” sorusu ikinciyi ister.

Kayıtlı belgelerin ikinci bir sonucu daha vardır. Sunucu yalnız kaydettiği belgeleri çalıştırdığı için, istemcinin gönderdiği keyfî bir sorgu hiç ayrıştırılmaz. Maliyet sınırının koruduğu şeyin bir bölümü böylece kendiliğinden korunur: bilinmeyen bir belge çalıştırılmadan reddedilir.

Özet

  • GraphQL isteği üç biçimde taşınabilir: gövdede sorgu ile POST, adreste sorgu ile GET ve önceden kaydedilmiş belge kimliği ile GET.
  • Çalıştırılmayan istekler (ayrıştırma, denetim, sınır) 400 döner; çalıştırılan istekler alan düzeyinde hata içerse bile 200 döner, çünkü kısmi başarı başarısızlık değildir.
  • Tek uç noktaya yapılan POST istekleri tek bir önbellek anahtarına düşer; on iki isteğin hiçbiri önbellekten karşılanamaz ve kütükte hepsi tek satır türü olarak görünür.
  • Sorgunun adrese taşınması önbellek anahtarı sayısını kaynak temelli aktarımla eşitler; bedeli 154 karaktere çıkan adres uzunluğudur.
  • Kayıtlı belgeler adresi 65 karaktere indirir ve sunucunun yalnız izin verilen belgeleri çalıştırmasını sağlar.
  • Kaynak temelli kütük kaynağa, GraphQL kütüğü işleme göre gruplar; ikisi farklı sorulara yanıt verir.

Kurs Kapanışı

Bu kurs bir arayüzün tasarlanmasını dört aşamada ele aldı ve her aşamada aynı ölçütü kullandı: bir kararın karşılığı ölçülebiliyor mu?

API biçemleri kaynak temelli, uzak yordam çağrısı temelli ve sorgu temelli yaklaşımları karşılaştırdı. Aralarındaki fark bir üstünlük sıralaması değil, hangi tarafın ne kadar karar verdiğidir: kaynak temelli tasarımda yanıtın biçimine sunucu, sorgu temelli tasarımda istemci karar verir.

Kaynak ve sözleşme tasarımı kaynak temelli yaklaşımı sonuna kadar götürdü. Kaynak modeli, adres düzeni, yöntem seçimi, durum kodu eşlemesi, gövde adlandırması, sayfalama, filtreleme, kısmi yanıt, etkisizlik anahtarları ve bağlantı odaklı yanıtlar — hepsi tek bir soruya hizmet etti: istemci sunucunun ne yapacağını tahmin etmek zorunda kalmasın.

Hata, sürüm ve belge sözleşmenin zaman içindeki hâlini kurdu. Hata gövdesi tek biçime bağlandı, doğrulama hataları alan düzeyinde bildirildi, sürümleme biçemlerinin gerçek farkının aracılarda olduğu ölçüldü, kırıcılık istek ve yanıt yönünde ters kurallarla tanımlandı ve sürüm numarası şema farkından türetildi. Sonra sözleşme makine okunur hâle geldi: aynı tanımdan doğrulayıcı, belge, sözleşme sınaması ve sahte sunucu üretildi. Bu konunun kalıcı sonucu tek cümleye sığar — sözleşme, yazıldığı yerde denetlenebiliyorsa sözleşmedir.

GraphQL ayrıntısı sorgu temelli yaklaşımı ölçerek anlattı ve bunu kütüphaneye başvurmadan, kendi ayrıştırıcısını ve çalıştırıcısını yazarak yaptı. Tip sistemi sorguyu çalıştırmadan denetledi; üç işlem türünün yürütme kuralı tek kopyalı kitap üzerinde ölçüldü; çözücü çağrısı ile veri kaynağı turu ayrıldı ve N+1 sorunu 101 turdan 3 tura indirildi; parçaların çalışma anında yok olduğu, değişkenlerin ayrı belge sayısını sabitlediği gösterildi; imleç sayfalamanın ofsete üstünlüğü liste değişirken ölçüldü; alan düzeyi yetkilendirme ve kısmi hata kuruldu; zorunluluk işaretinin bir alanın çökmesini bütün yanıta yayabildiği gösterildi; ve maliyet sınırının derinlik sınırından neden daha temel olduğu sayıyla konuldu.

İki yaklaşımın ortak dersi de burada görünür. Kaynak temelli tasarımda tüketicinin ne istediğini sunucu tahmin eder ve fazla ya da eksik veri gönderir; sorgu temelli tasarımda tüketici tam olarak istediğini alır ama sunucu ne kadar iş yapacağını önceden bilemez. İlki tasarım zamanında, ikincisi çalışma zamanında ödenen bir bedeldir. Sözleşme testleri birinciyi, maliyet sınırı ikinciyi ölçülebilir kılar.

Bu kurs boyunca bir soru sürekli ertelendi. Ödünç kaydının notunu yalnız kaydın açıldığı şubenin görmesi gerektiğini yazdık ama isteği yapanın gerçekten o şubenin görevlisi olduğunu nereden bildiğimizi hiç sormadık; yetki kuralları bağlamdaki kullanici ve rol alanlarını okudu ve o alanların oraya nasıl geldiği açıkta kaldı. Kullanımdan kaldırma telemetrisinde tüketici kimliğinin bir başlıkla gelmesi de aynı boşluğun başka bir görüntüsüydü — istemcinin kendi kimliğini bildirmesi, kimlik doğrulama değildir.

Kimlik Doğrulama ve Yetkilendirme kursu bu boşluğu doldurur. Kimliğin nasıl kanıtlandığı ile o kimliğe neyin izin verildiğini birbirinden ayırır; parola ve belirteç saklamanın güvenli gerçekleştirimini kurar; ve yetki modelini rol, öznitelik ya da ilişki temelinde tanımlamanın ölçülebilir sonuçlarını karşılaştırır. Bu kursta yazdığımız yetki kuralları oraya taşındığında, bağlamın nasıl doldurulduğu artık bir varsayım olmayacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat