İçeriğe geç
academia.sh

Ders 26 / 34

Şema ve Tip Sistemi

Nesne, skaler, numaralandırma, arayüz ve birleşim tiplerinin tanımlanması, sorgu belgesinin ayrıştırılması ve şemanın sorguyu çalıştırmadan önce denetleyerek hataları bulması.

İçindekiler

Önceki konu boyunca sözleşme hep aynı biçimde kuruldu: sunucu hangi kaynağı hangi alanlarla vereceğine karar verdi, istemci verilenle yetindi. Kısmi yanıt ve alan seçimi bu katılığı gevşetmişti ama sınırı yine sunucu çiziyordu.

Sorgu temelli yaklaşım ilişkiyi tersine çevirir. Sunucu bir tip sistemi tanımlar; hangi alanların döneceğine istemci karar verir ve yanıt sorgunun biçimini alır. Bunun karşılığında sunucu bir güvence ister: istemcinin sorabileceği her şey tipler tarafından önceden tanımlanmış olmalıdır.

Bu konu boyunca ödünç servisi için küçük bir GraphQL çalıştırıcısı yazılacak. Bu ders çalıştırıcının ilk iki parçasını kurar — tip sistemi ve ayrıştırıcı — ve üçüncüsünü gösterir: şemanın sorguyu çalıştırmadan önce denetlemesi.

Beş Tip Türü

Şema, adlandırılmış tiplerden oluşan bir kayıttır. Beş tür yeter.

  • Skaler, bölünmeyen değerdir: kimlik, dizgi, sayı, tarih. Alt seçim alamaz.
  • Numaralandırma (enum), sabit bir değer kümesidir. Skaler gibi yapraktır.
  • Nesne, adlandırılmış alanları olan bileşik tiptir. Alt seçim zorunludur.
  • Arayüz, birden çok nesne tipinin paylaştığı alanları bildirir. Bir arayüz üzerinden yalnız ortak alanlar seçilebilir; tipe özgü alanlar için somut tipe inmek gerekir.
  • Birleşim, ortak alanı olmayan tiplerin bir arada dönmesini sağlar. Birleşim üzerinden hiçbir alan doğrudan seçilemez.

Arayüz ile birleşim arasındaki fark, sorgunun neye izin verdiğidir. Ödünç kaydı ile üye kaydının ikisi de bir kimlik ve oluşturma tarihi taşır; bu ortaklık bir arayüzdür. Arama sonucu ise kitap veya üye döndürür ve bu ikisinin ortak bir alanı yoktur; bu bir birleşimdir.

// sema.mjs — odunc servisinin tip sistemi
// Tip turleri: skaler, sirali, nesne, arayuz, birlesim. Alan tipi dizgiyle yazilir:
// "Odunc", "ID!", "[Kalem!]!" — ! zorunlu, [] liste demektir.

export const SEMA = {
  sorgu: "Sorgu",
  mutasyon: "Mutasyon",
  tipler: {
    ID:      { tur: "skaler" },
    String:  { tur: "skaler" },
    Int:     { tur: "skaler" },
    Tarih:   { tur: "skaler" },
    Durum:   { tur: "sirali", degerler: ["ACIK", "KAPALI", "GECIKMIS"] },

    // Arayuz: birden cok tipin ortak alanlarini bildirir.
    Kayit:   { tur: "arayuz", alanlar: { id: "ID!", olusturma: "Tarih!" } },

    Kitap:   { tur: "nesne", alanlar: { isbn: "ID!", baslik: "String!", yazar: "String!", kopya: "Int!" } },
    Uye:     { tur: "nesne", arayuzler: ["Kayit"],
               alanlar: { id: "ID!", olusturma: "Tarih!", ad: "String!",
                          oduncler: { tip: "[Odunc!]!", argumanlar: { durum: "Durum" } } } },
    Kalem:   { tur: "nesne", alanlar: { kitap: "Kitap!", sube: "String!" } },
    Odunc:   { tur: "nesne", arayuzler: ["Kayit"],
               alanlar: { id: "ID!", olusturma: "Tarih!", uye: "Uye!", kalemler: "[Kalem!]!",
                          durum: "Durum!", iadeTarihi: "Tarih" } },

    // Birlesim: ortak alani olmayan tiplerin bir arada donmesi.
    AramaSonucu: { tur: "birlesim", uyeler: ["Kitap", "Uye"] },

    Sorgu:   { tur: "nesne", alanlar: {
                 odunc: { tip: "Odunc", argumanlar: { id: "ID!" } },
                 oduncler: { tip: "[Odunc!]!", argumanlar: { durum: "Durum" } },
                 uye:   { tip: "Uye",   argumanlar: { kimlik: "ID!" } },
                 arama: { tip: "[AramaSonucu!]!", argumanlar: { terim: "String!" } },
                 kayit: { tip: "Kayit", argumanlar: { id: "ID!" } } } },
    Mutasyon: { tur: "nesne", alanlar: {
                 oduncVer: { tip: "Odunc", argumanlar: { uye: "ID!", isbn: "ID!" } },
                 iadeAl:   { tip: "Odunc", argumanlar: { id: "ID!" } } } },
  },
};

// "[Kalem!]!" -> { ad: "Kalem", liste: true, zorunlu: true }
export function tipCoz(dizgi) {
  let s = dizgi, zorunlu = false, liste = false;
  if (s.endsWith("!")) { zorunlu = true; s = s.slice(0, -1); }
  if (s.startsWith("[")) { liste = true; s = s.slice(1, -1); if (s.endsWith("!")) s = s.slice(0, -1); }
  return { ad: s, liste, zorunlu };
}

export const alanTanimi = (tip, ad) => {
  const t = tip.alanlar?.[ad];
  return t === undefined ? null : typeof t === "string" ? { tip: t, argumanlar: {} } : { argumanlar: {}, ...t };
};

// Bir arayuz ya da birlesimin altina dusebilecek somut tipler
export const olasiTipler = (sema, ad) => {
  const t = sema.tipler[ad];
  if (t.tur === "birlesim") return t.uyeler;
  if (t.tur === "arayuz") return Object.entries(sema.tipler).filter(([, x]) => x.arayuzler?.includes(ad)).map(([a]) => a);
  return [ad];
};

Sorgu ve Mutasyon tiplerinin özel bir yanı yoktur; sıradan nesne tipleridir. Onları başlangıç noktası yapan, şemanın sorgu ve mutasyon alanlarında adlarının yazılı olmasıdır. Tip sistemi bir çizgedir ve bu iki ad, çizgeye girilen düğümleri gösterir.

Sorgunun Ayrıştırılması

Sorgu bir metindir; denetlenebilmesi için ağaca çevrilmelidir. Aşağıdaki ayrıştırıcı işlem türlerini, takma adları, argümanları, değişkenleri ve sorgu parçalarını (fragment) tanır; kısa biçimiyle parça denecek.

// ayristirici.mjs — GraphQL sorgu belgesini agaca cevirir
// Desteklenen: islem turleri, takma ad, arguman, degisken, parca ve satir ici parca.

const SIMGE = /\s+|,|#[^\n]*|(\.\.\.|[{}():$=[\]!]|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|[_A-Za-z][_0-9A-Za-z]*)/g;

const simgele = (metin) => [...metin.matchAll(SIMGE)].map((e) => e[1]).filter((s) => s !== undefined);

export function ayristir(metin) {
  const s = simgele(metin);
  let i = 0;
  const bak = () => s[i];
  const al = (beklenen) => {
    if (beklenen !== undefined && s[i] !== beklenen) throw new Error(`beklenen "${beklenen}", gelen "${s[i] ?? "belge sonu"}"`);
    return s[i++];
  };

  const deger = () => {
    if (bak() === "$") { al("$"); return { tur: "degisken", ad: al() }; }
    if (bak() === "[") { al("["); const d = []; while (bak() !== "]") d.push(deger()); al("]"); return { tur: "sabit", deger: d }; }
    const t = al();
    if (t.startsWith('"')) return { tur: "sabit", deger: JSON.parse(t) };
    if (/^-?\d/.test(t)) return { tur: "sabit", deger: Number(t) };
    if (t === "true" || t === "false") return { tur: "sabit", deger: t === "true" };
    return { tur: "sirali", deger: t };                        // sirali tip degeri
  };

  const tipAdi = () => {                                       // Odunc! [Kalem!]! gibi
    let ad;
    if (bak() === "[") { al("["); ad = `[${tipAdi()}]`; al("]"); } else ad = al();
    if (bak() === "!") { al("!"); ad += "!"; }
    return ad;
  };

  const secimKumesi = () => {
    al("{");
    const secim = [];
    while (bak() !== "}") {
      if (bak() === "...") {
        al("...");
        if (bak() === "on") { al("on"); const tip = al(); secim.push({ tur: "satirIci", tip, secim: secimKumesi() }); }
        else secim.push({ tur: "parcaYayimi", ad: al() });
        continue;
      }
      let ad = al(), takma = null;
      if (bak() === ":") { al(":"); takma = ad; ad = al(); }
      const argumanlar = {};
      if (bak() === "(") { al("("); while (bak() !== ")") { const a = al(); al(":"); argumanlar[a] = deger(); } al(")"); }
      const alt = bak() === "{" ? secimKumesi() : null;
      secim.push({ tur: "alan", ad, takma: takma ?? ad, argumanlar, secim: alt });
    }
    al("}");
    return secim;
  };

  const islemler = [], parcalar = {};
  while (i < s.length) {
    if (bak() === "fragment") {
      al("fragment"); const ad = al(); al("on"); const tip = al();
      parcalar[ad] = { ad, tip, secim: secimKumesi() };
      continue;
    }
    let tur = "query", ad = null;
    if (["query", "mutation", "subscription"].includes(bak())) { tur = al(); if (bak() !== "(" && bak() !== "{") ad = al(); }
    const degiskenler = [];
    if (bak() === "(") {
      al("(");
      while (bak() !== ")") {
        al("$"); const dad = al(); al(":"); const dtip = tipAdi();
        let varsayilan = undefined;
        if (bak() === "=") { al("="); varsayilan = deger().deger; }
        degiskenler.push({ ad: dad, tip: dtip, varsayilan });
      }
      al(")");
    }
    islemler.push({ tur, ad, degiskenler, secim: secimKumesi() });
  }
  return { islemler, parcalar };
}

Ayrıştırıcının ürettiği ağacın tek bilmediği şey anlamdır. gecikmeCezasi diye bir alan seçilmişse ayrıştırıcı bunu sorunsuzca ağaca koyar; böyle bir alanın olmadığını söyleyecek olan tip sistemidir.

Şemanın Sorguyu Denetlemesi

Denetleyici, seçim kümesini şemadaki tiplerle birlikte gezer ve her adımda uyum arar.

// denetle.mjs — sorguyu semaya karsi dogrular; calistirmadan once hatalari bulur
import { tipCoz, alanTanimi, olasiTipler } from "./sema.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

export function denetle(sema, belge) {
  const hatalar = [];
  for (const islem of belge.islemler) {
    const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
    const bildirilen = new Set(islem.degiskenler.map((d) => d.ad));
    secimDenetle(sema, belge, kok, islem.secim, kok, bildirilen, hatalar);
  }
  return hatalar;
}

function secimDenetle(sema, belge, tipAdi, secim, yol, bildirilen, hatalar) {
  const tip = sema.tipler[tipAdi];
  for (const s of secim) {
    if (s.tur === "parcaYayimi") {
      const parca = belge.parcalar[s.ad];
      if (!parca) { hatalar.push(`${yol}: "${s.ad}" adlı parça tanımlı değil`); continue; }
      if (!olasiTipler(sema, tipAdi).includes(parca.tip) && parca.tip !== tipAdi)
        hatalar.push(`${yol}: "${s.ad}" parçası ${parca.tip} için, buradaki tip ${tipAdi}`);
      else secimDenetle(sema, belge, parca.tip, parca.secim, `${yol}...${s.ad}`, bildirilen, hatalar);
      continue;
    }
    if (s.tur === "satirIci") {
      if (!olasiTipler(sema, tipAdi).includes(s.tip))
        hatalar.push(`${yol}: ${s.tip}, ${tipAdi} altına düşemez (olası tipler: ${olasiTipler(sema, tipAdi).join(", ")})`);
      else secimDenetle(sema, belge, s.tip, s.secim, `${yol}...${s.tip}`, bildirilen, hatalar);
      continue;
    }

    // Birlesim tipinde dogrudan alan secilemez; once somut tipe inilmelidir.
    if (tip.tur === "birlesim") { hatalar.push(`${yol}: ${tipAdi} bir birleşimdir, "${s.ad}" alanı doğrudan seçilemez`); continue; }

    const tanim = alanTanimi(tip, s.ad);
    if (!tanim) { hatalar.push(`${yol}: ${tipAdi} tipinde "${s.ad}" alanı yok`); continue; }

    for (const a of Object.keys(s.argumanlar)) {
      if (!(a in tanim.argumanlar)) hatalar.push(`${yol}.${s.ad}: "${a}" adlı argüman tanımlı değil`);
    }
    for (const d of Object.values(s.argumanlar)) {
      if (d.tur === "degisken" && !bildirilen.has(d.ad)) hatalar.push(`${yol}.${s.ad}: $${d.ad} değişkeni bildirilmemiş`);
    }
    for (const [a, t] of Object.entries(tanim.argumanlar)) {
      if (t.endsWith("!") && !(a in s.argumanlar)) hatalar.push(`${yol}.${s.ad}: zorunlu "${a}" argümanı verilmemiş`);
    }

    const alanTipi = sema.tipler[tipCoz(tanim.tip).ad];
    const yaprakMi = YAPRAK.has(alanTipi.tur);
    if (yaprakMi && s.secim) hatalar.push(`${yol}.${s.ad}: ${tipCoz(tanim.tip).ad} bir yapraktır, alt seçim alamaz`);
    else if (!yaprakMi && !s.secim) hatalar.push(`${yol}.${s.ad}: ${tipCoz(tanim.tip).ad} bileşiktir, alt seçim zorunludur`);
    else if (!yaprakMi) secimDenetle(sema, belge, tipCoz(tanim.tip).ad, s.secim, `${yol}.${s.ad}`, bildirilen, hatalar);
  }
}
// kosu.mjs — bir gecerli, alti gecersiz sorguyu semaya karsi denetler
import { ayristir } from "./ayristirici.mjs";
import { denetle } from "./denetle.mjs";
import { SEMA } from "./sema.mjs";

const SORGULAR = {
  "geçerli sorgu": `
    query OduncEkrani($uye: ID!) {
      uye(kimlik: $uye) { ad oduncler(durum: ACIK) { ...Ozet } }
      arama(terim: "algoritma") { ... on Kitap { baslik } ... on Uye { ad } }
      kayit(id: "O-1") { id olusturma }
    }
    fragment Ozet on Odunc { id durum kalemler { kitap { baslik } } }`,

  "olmayan alan": `{ odunc(id: "O-1") { id gecikmeCezasi } }`,
  "yaprağa alt seçim": `{ odunc(id: "O-1") { durum { ad } } }`,
  "bileşiğe alt seçim yok": `{ odunc(id: "O-1") { id uye } }`,
  "birleşimden doğrudan alan": `{ arama(terim: "x") { baslik } }`,
  "yanlış parça tipi": `{ odunc(id: "O-1") { ...KitapOzeti } } fragment KitapOzeti on Kitap { baslik }`,
  "eksik argüman ve bildirilmemiş değişken": `{ odunc { id } uye(kimlik: $k) { ad } }`,
};

for (const [ad, metin] of Object.entries(SORGULAR)) {
  const hatalar = denetle(SEMA, ayristir(metin));
  console.log(`\n${ad}  (${hatalar.length} hata)`);
  for (const h of hatalar) console.log(`  ${h}`);
}
geçerli sorgu  (0 hata)

olmayan alan  (1 hata)
  Sorgu.odunc: Odunc tipinde "gecikmeCezasi" alanı yok

yaprağa alt seçim  (1 hata)
  Sorgu.odunc.durum: Durum bir yapraktır, alt seçim alamaz

bileşiğe alt seçim yok  (1 hata)
  Sorgu.odunc.uye: Uye bileşiktir, alt seçim zorunludur

birleşimden doğrudan alan  (1 hata)
  Sorgu.arama: AramaSonucu bir birleşimdir, "baslik" alanı doğrudan seçilemez

yanlış parça tipi  (1 hata)
  Sorgu.odunc: "KitapOzeti" parçası Kitap için, buradaki tip Odunc

eksik argüman ve bildirilmemiş değişken  (2 hata)
  Sorgu.odunc: zorunlu "id" argümanı verilmemiş
  Sorgu.uye: $k değişkeni bildirilmemiş

Yedi sorgudan biri geçti, altısı reddedildi ve hiçbiri çalıştırılmadı. Reddetmelerin tamamı yalnızca şemadan gelen bilgiyle yapıldı; veri kaynağına bakılmadı, tek bir çözücü çağrılmadı.

Bu, kaynak temelli tasarımdan yapısal olarak farklı bir noktadır. Orada bir uç noktanın neyi kabul ettiği çalışma anında doğrulanır ve tüketici hangi alanların bulunduğunu belgeden öğrenir. Burada aynı bilgi şemada durur ve sorgu gönderilmeden denetlenebilir; makine okunur belgeler dersinde ayrıca yazılan tanım, tip sisteminin kendisidir.

Tip Sisteminin Kazandırdığı ve İstediği

Arayüz ve birleşim satırları, tip sisteminin en çok iş gördüğü yerlerdir. kayit(id:) alanı bir arayüz döndürür; sorgu ondan yalnız id ve olusturma alanlarını seçebilir, çünkü arayüzün bildirdiği alanlar bunlardır. Ödünç kaydına özgü durum alanını okumak isteyen sorgu, ... on Odunc yazarak somut tipe inmek zorundadır. Birleşimde ise ortak alan hiç yoktur; her dal kendi tipini bildirmeden hiçbir şey okuyamaz.

Bu katılığın karşılığı, yanıtın biçiminin sorgudan çıkarılabilmesidir. İstemci gönderdiği seçim kümesine bakarak dönecek gövdenin şeklini bilir; sunucunun ayrıca bir yanıt şeması yayımlamasına gerek kalmaz.

İstenen bedel ise şemanın eksiksiz olmasıdır. Şemada tanımlanmamış hiçbir şey sorulamaz; “şimdilik ham veriyi olduğu gibi döndürelim” gibi bir kaçış yolu yoktur. Ödünç servisinin her alanı, her argümanı ve her tip ilişkisi yazılmak zorundadır.

Özet

  • Şema beş tip türünden kurulur: skaler ve numaralandırma yapraktır, nesne bileşiktir, arayüz ortak alanları bildirir, birleşim ortak alanı olmayan tipleri bir arada döndürür.
  • Arayüz üzerinden yalnız ortak alanlar okunur; birleşim üzerinden hiçbir alan doğrudan okunamaz, her iki durumda da tipe özgü alanlar için somut tipe inilir.
  • Sorgu ve Mutasyon sıradan nesne tipleridir; onları başlangıç noktası yapan, şemada adlarının giriş düğümü olarak yazılmış olmasıdır.
  • Ayrıştırıcı sorguyu ağaca çevirir ama anlamını bilmez; alanın var olup olmadığını söyleyen tip sistemidir.
  • Şemaya karşı denetim, sorgu çalıştırılmadan ve tek bir çözücü çağrılmadan yapılır; olmayan alan, yaprağa alt seçim, bileşiğe eksik seçim, yanlış parça tipi ve eksik argüman bu aşamada yakalanır.
  • Tip sisteminin karşılığı yanıt biçiminin sorgudan çıkarılabilmesi, bedeli ise şemada tanımlanmamış hiçbir şeyin sorulamamasıdır.

Sonraki Adım

Denetlenen sorgu henüz çalıştırılmadı; şema neyin sorulabileceğini söylüyor ama yanıtın nereden geleceğini söylemiyor. Ayrıca buraya kadar yalnız tek bir işlem türü konuşuldu. Ödünç vermek bir okuma değil, bir değiştirmedir ve sunucudaki sırayla ilgili farklı bir güvence ister; bir kitabın durumunun değişmesini beklemek ise ne okuma ne değiştirmedir, zaman içinde birden çok yanıt üretir. Sonraki ders üç işlem türünü tanımlar, çalıştırıcının ilk hâlini yazar ve üçünün yürütme kuralı bakımından nerede ayrıldığını gösterir.

İ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