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.