Kavramlar, Fikirler, Stratejiler
Kavramlar, Fikirler, StratejilerUygulamayı Farklı GraphQL Sunucularıyla Çalışacak Şekilde Tasarlamak

Uygulamayı Farklı GraphQL Sunucularıyla Çalışacak Şekilde Tasarlamak

"Uygulamaları arayüzlere karşı, uygulamalara değil yazın" ilkesi; bir işlevi doğrudan çağırmak yerine, hangi girdilerin gerekli olduğunu ve beklenen çıktının ne olduğunu listeleyen bir sözleşme aracılığıyla çağırmak ve uygulamanın nasıl yapıldığını gizlemek anlamına gelir. Bu strateji, uygulamayı belirli bir implementasyondan, sağlayıcıdan veya yığından bağımsız kılmaya yardımcı olur; uygulama kodunu değiştirmek zorunda kalmadan bunlar arasında geçiş yapmayı mümkün kılar.

Bu stratejiyi GraphQL ile de uygulayabiliriz. GraphQL, uygulama ile sunucu arasında aracı görevi görebilir; tüm gerekli değişiklikleri yalnızca GraphQL queries üzerinde yapmamızı, iş mantığını ise dokunulmaz bırakmamızı sağlar.

Bir GraphQL query'si, istemci ile sunucu arasında bir arayüz görevi görür. Bir query çalıştırılırken, GraphQL sunucusu onu işler ve gerekli verileri istemciye döndürür. Veriler nereden geliyor? Nasıl elde edildi? İstemci bilmez ve önemsemez.

GraphQL query'si, istemci ile sunucu arasında bir arayüz görevi görür

Query'ye verilen yanıt, query ile aynı şekle sahip olacaktır. Bu GraphQL query'si için:

{
  post(by: { id: 1 }) {
    id
    title
  }
}

...yanıt şu şekilde olacaktır:

{
  "data": {
    "post": {
      "id": 1,
      "title": "Hello world!"
    }
  }
}

Aynı query farklı parametrelerle verildiğinde, döndürülen veriler farklı olacaktır; ancak şekil sabit kalacaktır. Bu, query değişmediği sürece uygulamanın verileri nasıl okuyup işleyeceğine dair mantığını değiştirmesi gerekmediği ve benzer şekilde hangi GraphQL sunucusunun query'yi çalıştırdığının önemi olmayacağı anlamına gelir.

Dolayısıyla bir GraphQL sunucusunu başka biriyle sorunsuzca değiştirebiliriz.

Queries, GraphQL Şemasına Bağlıdır

Ancak son paragraf biraz fazla iyimser, çünkü GraphQL query'sinin GraphQL sunucusuna bağlı olarak değişmesi gerekebilir. Daha kesin olmak gerekirse, query GraphQL şemasına dayanır; farklı sunucular farklı şemalar sunuyorsa query de farklı olacaktır.

Örneğin, Cursor Connections Specification kullanan bir GraphQL sunucusu şu query'yi çalıştırabilir:

{
  categories(first: 10000) {
    edges {
      node {
        categoryId
        description
        id
        name
        slug
      }
    }
  }
}

WordPress tarzı sayfalama kullanan başka bir sunucu (Gato GraphQL gibi) ise aynı query'yi şu şekilde çalıştıracaktır:

{
  postCategories(pagination: { limit: 10000 }) {
    id
    description
    globalID
    name
    slug
  }
}

İki query arasındaki farkları görebiliriz:

ÖzellikSunucu #1Sunucu #2
Yazı kategorileri alanıcategoriespostCategories
Sonuç sayısını sınırlamak için alan argümanıfirstpagination.limit
Bir nesnenin id alanı şunu temsil ederbenzersiz global ID'sitipi için benzersiz ID'si
Query'nin şekliedges.node nedeniyle daha derindaha düz

Uygulamadaki birinci sunucuya ait query'yi ikinci sunucunun eşdeğeriyle değiştirmek tek başına işe yaramaz. Bunun nedeni, mantığın yanıttaki verilere orijinal query'nin şekline ve alanlarına göre erişmeye devam etmesidir.

Olası bir çözüm, istemcideki veri alma mantığını da değiştirmektir. Örneğin, aşağıdaki mantık:

const categories = data?.data.categories.edges.map(({ node = {} }) => node);

...şu şekilde değiştirilebilir:

const categories = data?.data.postCategories;

Ama tam olarak kaçınmak istediğimiz şey bu. Değişiklikleri minimum düzeyde tutmak, yalnızca arayüzü (GraphQL query'sini) değiştirmek ve iş mantığını değiştirmeden korumak istiyoruz.

Neyse ki, yalnızca GraphQL queries değiştirilerek farkları kapatmak mümkündür; aşağıdaki adımlar izlenerek:

  1. GraphQL queries'i uygulamadan bağımsız tutmak
  2. Alan adlarını alias'lar aracılığıyla uyarlamak
  3. Yanıtın şeklini bir self alanı aracılığıyla uyarlamak

Bu 3 adım aracılığıyla bir uygulamayı farklı bir GraphQL sunucusuna nasıl yönlendirebileceğimizi görelim.

GraphQL Queries'i Uygulamadan Bağımsız Tutmak

GraphQL queries'i uygulama mantığından ayırmak şunları gerektirir:

  • Her GraphQL query'sini (veya bir grubunu) ayrı bir dosyada ve hepsini belirli bir klasörde saklamak
  • Queries'i dışa aktarmak ve uygulamaya içe aktarmak

Örneğin, her GraphQL query'sini src/data altında ayrı bir dosyaya yerleştirebilir ve dışa aktarabiliriz:

// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
  {
    categories(first: 10000) {
      edges {
        node {
          databaseId
          description
          id
          name
          slug
        }
      }
    }
  }
`;

Uygulama daha sonra GraphQL query'sini içe aktarabilir ve kullanabilir:

import { QUERY_ALL_CATEGORIES } from 'data/categories';
 
export async function getAllCategories() {
  const apolloClient = getApolloClient();
 
  const data = await apolloClient.query({
    query: QUERY_ALL_CATEGORIES,
  });
 
  const categories = data?.data.categories.edges.map(({ node = {} }) => node);
 
  return {
    categories,
  };
}

Bu yapı sayesinde, tüm değişikliklerin yalnızca src/data altındaki dosyalarda yapılması gerekmektedir.

Alan Adlarını Alias'lar Aracılığıyla Uyarlamak

Bir alan alias'ı, ikinci GraphQL sunucusunun yanıtındaki bir alanı birinci sunucudaki o alanın adıyla yeniden adlandırmak için kullanılabilir.

Bu sayede postCategories, id ve globalID alanları, uygulamanın beklediği adlar kullanılarak alınabilir: sırasıyla categories, categoryId ve id:

{
  categories: postCategories(pagination: { limit: 10000 }) {
    categoryId: id
    description
    id: globalID
    name
    slug
  }
}

categories alanının first argümanına sahip olduğunu, buna karşılık gelen postCategories alanının ise pagination.limit argümanını kullandığını lütfen unutmayın. Ancak alan argümanları yanıttaki alan adına yansımadığından, bunlar hakkında endişelenmemize gerek yoktur.

Yanıtın Şeklini Bir self Alanı Aracılığıyla Uyarlamak

Son zorluk biraz daha karmaşıktır: Cursor Connections spec'inden gelen edges ve node için ek seviyeleri ekleyerek yanıtın şeklini değiştirmemiz gerekmektedir.

Bunu başarmak için, GraphQL şemasındaki tüm tiplere uygulandığı nesneyi geri yansıtan bir self alanı ekleyeceğiz:

type QueryRoot {
  self: QueryRoot!
}
 
type Post {
  self: Post!
}
 
type User {
  self: User!
}

self alanı, sorgulanan nesneden ayrılmadan query'ye ek seviyeler eklenmesine olanak tanır. Bu query çalıştırıldığında:

{
  __typename
  self {
    __typename
  }
  
  post(by: { id: 1 }) {
    self {
      id
      __typename
    }
  }
  
  user(by: { id: 1 }) {
    self {
      id
      __typename
    }
  }
}

...şu yanıt üretilir:

{
  "data": {
    "__typename": "QueryRoot",
    "self": {
      "__typename": "QueryRoot"
    },
    "post": {
      "self": {
        "id": 1,
        "__typename": "Post"
      }
    },
    "user": {
      "self": {
        "id": 1,
        "__typename": "User"
      }
    }
  }
}

Artık nodes ve edge seviyelerini yapay olarak eklemek için self kullanabiliriz:

{
  categories: self {
    edges: postCategories(pagination: { limit: 10000 }) {
      node: self {
        categoryId: id
        description
        id: globalID
        name
        slug
      }
    }
  }
}

edges ve self için GraphQL şemasındaki nesnenin türü açıkça farklıdır. Ancak bu, uygulama için önemli değildir; çünkü uygulama, GraphQL sunucusunda modellenen gerçek nesneyle etkileşime girmez. Bunun yerine, verileri bir JSON nesnesi olarak alır ve bir PostConnection ya da Post nesnesinden gelen bir alan için o veri parçası aynı olacaktır.

categories alanının self aracılığıyla, edges alanının ise postCategories aracılığıyla çözümlendiğini (tersinin değil) lütfen unutmayın. Bu, döndürülen öğelerin kardinalitesinin Cursor Connections spec'ini kullanan alanlar tarafından tanımlananla uyuşmasını sağlamak içindir:

type RootQuery {
  categories: RootQueryToCategoryConnection
}
 
type RootQueryToCategoryConnection {
  edges: [RootQueryToCategoryConnectionEdge]
}
 
type RootQueryToCategoryConnectionEdge {
  node: Category
}

Uyarlanmış GraphQL query'si tersine olsaydı (yani categories: postCategories ve edges: self şeklinde sorgulanıyor olsaydı), verilere erişim başarısız olurdu; çünkü data.categories bir dizi olurdu ve şu işlem çalıştırılırken data.categories.edges bir hataya yol açardı:

const categories = data?.data.categories.edges.map(({ node = {} }) => node);

Tüm Queries'i Uyarlamak

Aynı strateji src/data içindeki tüm GraphQL queries'e uygulandıktan sonra, uygulama bir GraphQL sunucusundan diğerine kolayca geçebilir.