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"
]
}
}
}
- Özel bir “kök” obje ile başlarız,
- Sonrasında
hero
alanını seçeriz, hero
alanından dönen obje içindename
veappearsIn
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
veappearsIn
Character
tipi içerisindeki alanlardır. Bu da demektir kiCharacter
tipi için yapılan sorgulardaname
veappearsIn
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ınID
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;
- Bu tipteki herhangi bir argümanın izin verilen değerler içinde olduğunu doğrulayabilirsiniz
- 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 daStarship
tü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)