GraphQL Şemalar ve Tipler

Önceki Sayfa > Sorgu ve Değişimler (mutations)

Bu sayfada GraphQL’in tip sistemi ve sorgulanabilecek veriyi nasıl tanımladığı ile ilgili bilmeniz gereken her şeyi öğreneceksiniz. GraphQL herhangi bir backend framework’ü yada programlama dili ile kullanılabileceği için implementasyona özel detaylardan uzak durup sadece konsept üzerine konuşacağız.

Tip Sistemi

Daha önce bir GraphQL sorgusu gördüyseniz bilirsiniz ki GraphQL sorgu dili en basit tanımıyla objeler üzerindeki alanları seçmekten ibarettir. Yani örneğin aşağıdaki sorguda:

sorgu:

{
hero {
name
appearsIn
}
}

cevap:

{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
  1. Özel bir “kök” obje ile başlarız,
  2. Sonrasında hero alanını seçeriz,
  3. heroalanından dönen obje içinde name ve appearsIn alanlarını seçeriz.

GraphQL’in sorgu yapısı dönen sonucun yapısına çok yakın olduğu için sunucu ile ilgili çok bir şey bilmeseniz de nasıl bir sonuç elde edebileceğinizi rahatlıkla tahmin edersiniz. Ancak sorgulayabileceğimiz veri ile ilgili detaylı bir tanıma sahip olmak (hangi alanları çağırabiliriz?, hangi türde objeler dönebilir?, alt objelerde hangi alanlar vardır?..)çok faydalıdır. İşte Şema burada devreye girer.

Tüm GraphQL servisleri bu servis aracılığıyla sorgulayabileceğiniz veri ile ilgili detaylı tanımlamaları içeren bir tip tanımlaması yapar. Sonrasında, sorgular geldiğinde bu tiplere göre doğrulamayı geçerse çalıştırılırlar.

Tip Dili

GraphQL servisleri herhangi bir programa dili ile yazılabilir. GraphQL’in şemaları üzerinde konuşmak için Javascript gibi tek bir dilin sentaksına bel bağlayamayacağımız için kendi basit dilimizi tanımlayacağız — “GraphQL Şema Dili”. Bu yapı sorgu diline çok benzeyecek ve dilden bağımsız olarak GraphQL şemalarından bahsedebilmemize olanak tanıyacak.

Obje Tipleri ve Alanlar

GraphQL şemalarının en basit bileşenleri servisinizden çekebileceğiniz bir obje türünü ve içindeki alanları temsil eden obje tipleridir. GraphQL şema dilinde bunu şöyle kullanabiliriz:

type Character {
name: String!
appearsIn: [Episode]!
}

Dil gayet okunabilir ancak ortak bir anlayış oluşturabilmek için üstünden geçelim:

  • Character içinde bazı alanlar içeren bir GraphQL Obje tipidir. Şemanızdaki çoğu tip obje tipinde olacaktır.
  • name ve appearsIn Character tipi içerisindeki alanlardır. Bu da demektir ki Character tipi için yapılan sorgulardaname ve appearsIn haricinde bir alan görmeyeceğiz.
  • String GraphQL implementasyonu ile gelen skalar tiplerden biridir. -bu tipler tek bir skalar objeye çözümlenir ve sorgu içerisinde alt seçimlere müsaade etmez. Skalar tipler ile ilgili daha sonra detaylı konuşacağız.
  • String! alanın boş bırakılamayacağını belirtir, bu alana yapılacak sorguda GraphQL’in her zaman null dışında bir değer döneceğinden emin olabiliriz. Tip dilinde bu durumu ünlem işareti ile belirtiyoruz.
  • [Episode]! Episode objelerinden oluşan bir dizgeyi temsil eder. Ayrıca ünlem işareti aldığı için,appearsIn alanını sorguladığınızda geriye boş da olsa bir dizge alacağınızdan emin olabilirsiniz.

Artık GraphQL obje tipinin nasıl göründüğünü ve GraphQL tip dilini basitçe nasıl okuyacağınızı biliyorsunuz.

Argümanlar (arguments)

GraphQL obje tipindeki her alan sıfır veya daha fazla argüman alabilir. Aşağıdaki örnekte length alanına bakalım:

type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METRE): Float
}

Tüm argümanlar isimlendirilmiş. Javascript ve Python gibi methodları sıralı argümanlar olarak alan dillerden farklı olarak GraphQL’deki tüm argümanlar isimleriyle birlikte beslenir. Bu örnekte length alanı unit olarak tanımlanmış bir argümana sahip.

Argümanlar zorunlu veya opsiyonel olabilir. Bir argüman opsiyonel olduğunda varsayılan bir değer tanımlayabiliriz. unit argümanı sorguya geçilmediğinde varsayılan olarak METRE değerini alacaktır.

Sorgu ve Mutation Tipleri

Şemanızdaki bir çok tip normal obje tipinde olacaktır, fakat şemaya özel iki ayrı tip bulunmaktadır:

schema {
query: Query
mutation: Mutation
}

Her GraphQL servisinde mutlaka bir query tipi vardır ancak mutation tipi olmak zorunda değildir. Bu tipler de sıradan obje tipleriyle aynıdır, özel olmalarının sebebi her GraphQL sorgusunun giriş noktası olmalarıdır. Eğer aşağıdaki gibi bir sorgu görürseniz:

sorgu:

query {
hero {
name
}
droid(id: "2000") {
name
}
}

cevap:

{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}

GraphQL servisinin içinde hero ve droid alanlarını barındıran bir Query tipi olduğunu anlamalısınız.

type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}

Mutation’lar da benzeri bir şekilde çalışır. Mutation tipinde alanlar tanımlarsınız ve bu alanlar ihtiyaç duyduğunuzda sorgunuzda çağırabileceğiniz kök mutation alanlarıdır.

Şemadaki özel giriş alanları olmalarının dışında Query ve Mutation tiplerinin diğer objelerden farkı olmadığını ve bunların alt alanlarının da birebir aynı şekilde çalıştığını hatırlamak önemlidir.

Skalar Tipler (scalar)

Bir GraphQL objesinin ismi ve alanları olur, eninde sonunda bu alanların veri dönmesi gerekmektedir. Bu noktada skalar tipler devreye girer, sorgunun yapraklarını temsil ederler.

Aşağıdaki sorguda name ve appearsIn alanları skalar tipte veri döndürür:

sorgu:

{
hero {
name
appearsIn
}
}

cevap:

{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}

Bunu biliyoruz çünkü bu alanların herhangi bir alt alanı bulunmamakta — yani sorgunun yaprakları.

GraphQL bir seri varsayılan skalar tipi ile gelmektedir:

  • Int
  • Float
  • String
  • Boolean
  • ID: ID scalar tipi eşsiz bir tanımlayıcıyı temsil eder, bir objeyi yeniden sorgulamak yada cache’lerken anahtar olarak kullanılır. ID tipi string gibi değerlendirilir ancak alanın ID olarak tanımlanması insanlar tarafından okunabilir olmaması gerektiğini belirtir.

Çoğu GraphQL servis implementasyonunda özel skalar tipler belirtmek mümkündür. Örneğin bir Date tipi tanımlayıp kullanabiliriz:

scalar Date

Bu tipin nasıl serialize, deserialize ve validate edileceğine implementasyonumuzda karar verebiliriz. Örneğin Date tipinin her zaman integer olan bir zaman damgasına serialize olması gerektiğini implementasyonda belirtebiliriz. Bu sayede istemciniz her zaman hangi formatta veriye ulaşacağını bilir.

Enumeration Tipleri

Aynı zamanda Enum olarak da bilinen enumeration tipleri sadece izin verilen belirli değerler alabilen özel bir skalar türüdür. Bu sayede;

  1. Bu tipteki herhangi bir argümanın izin verilen değerler içinde olduğunu doğrulayabilirsiniz
  2. Tip sistemi içerisinde bu alanın her zaman önceden belirlenmiş değerler arasında olacağından emin olabilirsiniz.

GraphQL şema dilinde bir enum tanımı aşağıdaki gibi gözükür:

enum Episode {
NEWHOPE
EMPIRE
JEDI
}

Bu da demektir ki şemamız içinde Episode tipini nerede kullanırsak kullanalım dönecek değerin tam olarak NEWHOPE, EMPIRE, ya daJEDI olacağından emin olabiliriz.

Çeşitli diller kullanılarak hazırlanabilen GraphQL servis implementasyonlarının dile özel enum yazma yöntemleri olabileceğini unutmayalım. Enum’ları kendi içinde destekleyen diller bu durumdan faydalanabilir; Javascript gibi enum desteği olmayan diller enum içerisindeki değerleri integer’lere mapleyebilir. Ancak bu eşlemeler istemci tarafına sızmaz, istemci tarafında gözüken bu enum değerleri temsil eden string türündeki isimleri olacaktır.

Listeler ve Null Olamayacak Değerler

GraphQL’de tanımlayabileceğiniz tipler sadece Obje tipleri, skalar tipler ve enumlardır. Ancak bu tipleri şemanın başka yerlerinde ya da sorgu değişken tanımlarınızda kullanmak istediğinizde bu alanların validasyonunu etkileyen ek tip niteleyicileri (modifiers) uygulayabilirsiniz. Bir örnekle görelim:

type Character {
name: String!
appearsIn: [Episode]!
}

Burada String tipini kullanıyoruz ve sonuna ünlem işareti (!) ekleyerek alanın Null olamayacağını belirtiyoruz. Bu sayede sunucumuz bu alan için her zaman null olmayan bir değer dönmeyi bekleyecek ve olur da null bir değer üretirse bir GraphQL çalıştırma hatası fırlatacak, bu sayede de istemci bir şeylerin doğru gitmediğini anlayacak.

Null olamayacak tip niteleyicisi aynı zamanda alana beslenen argümanlar için de kullanılabilir, sorgu anında bu alan boş bırakılmışsa sunucu GraphQL doğrulama hatası döndürecektir.

sorgu:

query DroidById($id: ID!) {
droid(id: $id) {
name
}
}

değişken tanımı:

{
"id": null
}

cevap:

{
"errors": [
{
"message": "Variable \"$id\" of required type \"ID!\" was not provided.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}

Listeler de benzer şekilde çalışır. Bir tip niteleyicisi kullanarak herhangi bir alanı List olarak işaretleyebiliriz, bu sayede bu alan belirtilen tipte bir dizge dönecektir. Şema dilinde bu tipi köşeli parantezler [ ve] ile sararak tetiklenir. Aynı şey argümanlar için de geçerlidir, doğrulama yapılırken bu değer için bir dizge beklenmesine sebep olur.

Liste niteleyicisi ve Null olmayacak niteleyicisi birleştirilebilir. Örneğin Null olmaya stringlerden oluşan bir listeniz olabilir:

myField: [String!]

Bu demektir ki listenin kendisi null olabilir, ancak null olan herhangi bir eleman içeremez. JSON olarak görelim:

myField: null // geçerli
myField: [] // geçerli
myField: ['a', 'b'] // geçerli
myField: ['a', null, 'b'] // hata

Şimdi de stringlerden oluşan ve null olmayan bir liste tanımlayalım:

myField: [String]!

Bu demektir ki listenin kendisi null olamaz ancak null değerler içerebilir:

myField: null // hata
myField: [] // geçerli
myField: ['a', 'b'] // geçerli
myField: ['a', null, 'b'] // geçerli

İhtiyaçlarınız doğrultusunda dilediğiniz kadar Null olmayacak ve Liste niteleyicisini ardı ardına kullanabilirsiniz.

Interface’ler

Bir çok tip sisteminde olduğu gibi GraphQL’de interface’leri destekler. Interface’ler bir tipin bu interface’i kullanmak için içermesi gereken alanları belirleyen soyut tiplerdir.

Örneğin Star Wars üçlemesindeki herhangi bir karakteri niteleyenCharacter adında bir interface’iniz olabilir:

interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}

Character interface’ini kullanan tüm tiplerin bu alanları (argümanları ve dönen tipleri de dahil olmak üzere) kullanması gerekecektir.

Örneğin aşağıda Character interface’in kullanan bir kaç tip tanımı görebilirsiniz:

type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}

Gördüğünüz gibi her iki tip de Character interface’indeki tüm alanları kullanıyor, aynı zamanda bir kaç yeni alan da içeriyor. totalCredits, starships veprimaryFunction alanları belirli tipte karakterlere özel alanlar.

Interface’ler özellikle birden fazla tipe ait bir obje ya da obje listesi dönmek istediğinizde faydalıdır.

Örneğin aşağıdaki sorgunun ürettiği hataya bakalım:

sorgu:

query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
primaryFunction
}
}

değişken tanımı:

{
"ep": "JEDI"
}

cevap:

{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}

hero alanı Character tipinde bir cevap döndürür , bu da episode argümanına göre sonuç ya Human ya da Droid olacak demektir. Üstteki sorguda sadece Character interface’i içinde olabilecek alanlar ile sorgulama yapabilirsiniz, ki bu alanlar içerisinde primaryFunction bulunmuyor.

Belirli obje türündeki alanları sorgulamak istediğinizde satır-içi fragment kullanmanız gerekmektedir.

sorgu:

query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}

değişken tanımı:

{
"ep": "JEDI"
}

cevap:

{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}

Bu konu ile ilgili detaylı bilgi için satır-içi fragmanlar bölümüne bakabilirsiniz.

Union tipleri

Union tipleri interface’lere çok benzer, ancak tipler arası paylaşılan alanları belirtmezler.

union SearchResult = Human | Droid | Starship

Şemamızda SearchResult tipini her döndüğümüzde Human, Droid, ya daStarshiptüründe bir sonuç elde edebiliriz. Union tiplerin içerdiği obje tiplerinin doğrudan objelere çözümlenebilir olması gerekmektedir, bu sebeple interface’leri ya da diğer union tiplerini kullanarak union tipi oluşturamazsınız.

SearchResult union tipi döndüren bir alanı sorguladığınızda herhangi bir alanla ilgili veri alabilmek için koşullu(conditional) bir fragment kullanmanız gerekmektedir:

sorgu:

{
search(text: "an") {
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}

cevap:

{
"data": {
"search": [
{
"name": "Han Solo",
"height": 1.8
},
{
"name": "Leia Organa",
"height": 1.5
},
{
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}

Input Obje Tipleri

Şu ana kadar alanlara argüman olarak hep enum ve string gibi skalar değerleri geçmek üzerine konuştuk. Ama kolaylıkla komplike objeler de kullanabiliriz. Bu durum özellikle mutationlar için önemlidir çünkü yaratılacak tüm objeyi geçme ihtiyacı duyarsınız. GraphQL şema dilinde input obje tipleri normal obje tipleri ile aynı gözükür, tek fark anahtar kelimenin type yerine input olmasıdır.

input ReviewInput {
stars: Int!
commentary: String
}

Aşağıda input obje tipini bir mutation içinde nasıl kullanabileceğinizi görebilirsiniz:

sorgu:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}

değişken tanımı:

{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}

cevap:

{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}

Input obje tipi içindeki alanlar da başka input obje tipinde olabilirler ancak şemanızda input ve output obje tiplerini karışık olarak kullanamazsınız. Input obje tipleri alanlarında argüman kabul etmezler.

Okumaya Devam Et > Doğrulama (validation)