İlk GraphQL API'nizi Oluşturma

Yayınlanan: 2022-03-11

Önsöz

Birkaç yıl önce Facebook, temel olarak veri sorgulama ve işleme için alana özgü bir dil olan GraphQL adlı arka uç API'leri oluşturmanın yeni bir yolunu tanıttı. İlk başta buna pek dikkat etmedim, ancak sonunda kendimi Toptal'da GraphQL'ye dayalı arka uç API'leri uygulamak zorunda kaldığım bir projeyle meşgul buldum. İşte o zaman devam ettim ve REST için öğrendiğim bilgileri GraphQL'e nasıl uygulayacağımı öğrendim.

Çok ilginç bir deneyimdi ve uygulama döneminde REST API'lerinde kullanılan standart yaklaşımları ve metodolojileri daha GraphQL dostu bir şekilde yeniden düşünmek zorunda kaldım. Bu yazıda, GraphQL API'lerini ilk kez uygularken göz önünde bulundurulması gereken genel sorunları özetlemeye çalışıyorum.

Gerekli Kitaplıklar

GraphQL, Facebook tarafından dahili olarak geliştirildi ve 2015'te herkese açık olarak yayınlandı. Daha sonra 2018'de, GraphQL projesi Facebook'tan, GraphQL sorgu dili spesifikasyonunu ve referansını koruyan ve geliştiren kar amacı gütmeyen Linux Vakfı tarafından barındırılan yeni kurulan GraphQL Vakfı'na taşındı. JavaScript için uygulama.

GraphQL hala genç bir teknoloji olduğundan ve JavaScript için ilk başvuru uygulaması mevcut olduğundan, onun için çoğu olgun kitaplık Node.js ekosisteminde bulunur. Ayrıca, GraphQL için açık kaynaklı araçlar ve kitaplıklar sağlayan Apollo ve Prisma adlı iki şirket daha var. Bu makaledeki örnek proje, JavaScript için GraphQL'nin referans uygulamasına ve bu iki şirket tarafından sağlanan kitaplıklara dayalı olacaktır:

  • Graphql-js – JavaScript için GraphQL'nin referans uygulaması
  • Apollo sunucusu – Express, Connect, Hapi, Koa ve daha fazlası için GraphQL sunucusu
  • Apollo-graphql-tools – SDL kullanarak bir GraphQL şeması oluşturun, taklit edin ve birleştirin
  • Prisma-graphql-middleware – GraphQL çözümleyicilerinizi ara yazılım işlevlerine ayırın

GraphQL dünyasında API'lerinizi GraphQL şemalarını kullanarak tanımlarsınız ve bunlar için belirtim The GraphQL Schema Definition Language (SDL) adlı kendi dilini tanımlar. SDL, aynı anda son derece güçlü ve etkileyici olmakla birlikte kullanımı çok basit ve sezgiseldir.

GraphQL şemaları oluşturmanın iki yolu vardır: kod öncelikli yaklaşım ve şema öncelikli yaklaşım.

  • Önce kod yaklaşımında, GraphQL şemalarınızı graphql-js kitaplığına dayalı JavaScript nesneleri olarak tanımlarsınız ve SDL kaynak koddan otomatik olarak oluşturulur.
  • Şema öncelikli yaklaşımda, GraphQL şemalarınızı SDL'de açıklar ve Apollo graphql-tools kitaplığını kullanarak iş mantığınızı bağlarsınız.

Şahsen ben şema öncelikli yaklaşımı tercih ediyorum ve bu makaledeki örnek proje için kullanacağım. Klasik bir kitapçı örneği uygulayacağız ve yazarlar ve kitaplar oluşturmak için CRUD API'lerinin yanı sıra kullanıcı yönetimi ve kimlik doğrulaması için API'ler sağlayacak bir arka uç oluşturacağız.

Temel GraphQL Sunucusu Oluşturma

Temel bir GraphQL sunucusunu çalıştırmak için yeni bir proje oluşturmalı, npm ile başlatmalı ve Babel'i yapılandırmalıyız. Babel'i yapılandırmak için önce aşağıdaki komutla gerekli kitaplıkları kurun:

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

Babel'i kurduktan sonra projemizin kök dizininde .babelrc isimli bir dosya oluşturun ve aşağıdaki konfigürasyonu buraya kopyalayın:

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

Ayrıca package.json dosyasını düzenleyin ve scripts bölümüne aşağıdaki komutu ekleyin:

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

Babel'i yapılandırdıktan sonra, aşağıdaki komutla gerekli GraphQL kitaplıklarını kurun:

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

Gerekli kitaplıkları kurduktan sonra, minimum kurulumla bir GraphQL sunucusu çalıştırmak için bu kod parçasını index.js dosyamıza kopyalayın:

 import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

Bundan sonra, npm run serve komutunu kullanarak sunucumuzu çalıştırabiliriz ve bir web tarayıcısında http://localhost:8080/graphql , GraphQL'nin Playground adlı etkileşimli görsel kabuğu açılacaktır. GraphQL sorgularını ve mutasyonlarını yürütün ve sonuç verilerini görün.

GraphQL dünyasında API işlevleri, sorgular, mutasyonlar ve abonelikler olarak adlandırılan üç kümeye ayrılır:

  • Sorgular , istemci tarafından sunucudan ihtiyaç duyduğu verileri istemek için kullanılır.
  • Mutasyonlar , istemci tarafından sunucuda veri oluşturmak/güncellemek/silmek için kullanılır.
  • Abonelikler , sunucuyla gerçek zamanlı bağlantı oluşturmak ve sürdürmek için istemci tarafından kullanılır. Bu, istemcinin sunucudan olayları almasını ve buna göre hareket etmesini sağlar.

Yazımızda sadece sorguları ve mutasyonları ele alacağız. Abonelikler çok büyük bir konudur; kendi makalelerini hak ederler ve her API uygulamasında gerekli değildir.

Gelişmiş Skaler Veri Tipleri

GraphQL ile oynadıktan çok kısa bir süre sonra, SDL'nin yalnızca ilkel veri türleri sağladığını ve her API'nin önemli bir parçası olan Tarih, Saat ve DateTime gibi gelişmiş skaler veri türlerinin eksik olduğunu keşfedeceksiniz. Neyse ki, bu sorunu çözmemize yardımcı olan bir kütüphanemiz var ve buna graphql-iso-date deniyor. Kurduktan sonra, şemamızda yeni gelişmiş skaler veri türleri tanımlamamız ve bunları kitaplık tarafından sağlanan uygulamalara bağlamamız gerekecek:

 import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };

Tarih ve saatin yanı sıra, kullanım durumunuza bağlı olarak sizin için yararlı olabilecek başka ilginç skaler veri türü uygulamaları da vardır. Örneğin, bunlardan biri, bize GraphQL şemamızda dinamik yazmayı kullanma ve API'mizi kullanarak yazılmamış JSON nesnelerini iletme veya döndürme yeteneği veren graphql-type-json'dur. Ayrıca, bize gelişmiş temizleme/doğrulama/dönüşüm ile özel GraphQL skalerlerini tanımlama yeteneği veren graphql-skalar kütüphanesi de bulunmaktadır.

Gerekirse, özel skaler veri türünüzü de tanımlayabilir ve yukarıda gösterildiği gibi şemanızda kullanabilirsiniz. Bu zor değil, ancak bunun tartışılması bu makalenin kapsamı dışındadır - eğer ilgileniyorsanız, daha gelişmiş bilgileri Apollo belgelerinde bulabilirsiniz.

Bölme Şeması

Şemanıza daha fazla işlevsellik ekledikten sonra, büyümeye başlayacak ve tüm tanım setini tek bir dosyada tutmanın imkansız olduğunu anlayacağız ve kodu organize etmek ve daha ölçeklenebilir hale getirmek için onu küçük parçalara ayırmamız gerekiyor. daha büyük bir boyut. Neyse ki şema oluşturucu işlevi, Apollo tarafından sağlanan makeExecutableSchema , şema tanımlarını ve bir dizi biçiminde çözümleyici haritalarını da kabul eder. Bu bize şemamızı ve çözümleyici haritamızı daha küçük parçalara ayırma yeteneği verir. Örnek projemde yaptığım tam olarak bu; API'yi aşağıdaki bölümlere ayırdım:

  • auth.api.graphql – Kullanıcı kimlik doğrulaması ve kaydı için API
  • author.api.graphql – Yazar girişleri için CRUD API
  • book.api.graphql – kitap girişleri için CRUD API'si
  • root.api.graphql – Şema kökü ve genel tanımlar (gelişmiş skaler tipler gibi)
  • user.api.graphql – Kullanıcı yönetimi için CRUD API

Bölme şeması sırasında dikkate almamız gereken bir şey var. Parçalardan biri kök şema olmalı, diğeri ise kök şemayı genişletmelidir. Bu karmaşık gelebilir, ancak gerçekte oldukça basittir. Kök şemada sorgular ve mutasyonlar şu şekilde tanımlanır:

 type Query { ... } type Mutation { ... }

Diğerlerinde ise şu şekilde tanımlanırlar:

 extend type Query { ... } extend type Mutation { ... }

Ve hepsi bu.

Kimlik doğrulama ve yetkilendirme

API uygulamalarının çoğunda, genel erişimi kısıtlama ve bir tür kural tabanlı erişim ilkeleri sağlama gereksinimi vardır. Bunun için kodumuzda şunları eklememiz gerekiyor: Kimlik doğrulama — kullanıcı kimliğini doğrulamak — ve Yetkilendirme , kural tabanlı erişim ilkelerini uygulamak için.

GraphQL dünyasında, REST dünyası gibi, genellikle kimlik doğrulama için JSON Web Token kullanıyoruz. Geçirilen JWT belirtecini doğrulamak için gelen tüm istekleri engellememiz ve üzerlerindeki yetkilendirme başlığını kontrol etmemiz gerekir. Bunun için, Apollo sunucusunun oluşturulması sırasında, tüm çözümleyiciler arasında paylaşılan bağlamı oluşturan mevcut istekle çağrılacak olan bir bağlam kancası olarak bir işlevi kaydedebiliriz. Bu şu şekilde yapılabilir:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

Burada, kullanıcı doğru bir JWT belirtecini iletecekse, bunu doğrularız ve kullanıcı nesnesini, istek yürütme sırasında tüm çözümleyiciler için erişilebilir olacak bağlamda saklarız.

Kullanıcı kimliğini doğruladık, ancak API'mize hala küresel olarak erişilebilir ve hiçbir şey, kullanıcılarımızın yetkilendirme olmadan onu aramasını engelleyemez. Bunu önlemenin bir yolu, kullanıcı nesnesini doğrudan her çözümleyicide bağlam içinde kontrol etmektir, ancak bu çok hataya açık bir yaklaşımdır çünkü çok sayıda ortak kod yazmamız gerekir ve yeni bir çözümleyici eklerken denetimi eklemeyi unutabiliriz. . REST API çerçevelerine bir göz atarsak, genellikle bu tür sorunlar HTTP istek durdurucuları kullanılarak çözülür, ancak GraphQL durumunda, bir HTTP isteği birden fazla GraphQL sorgusu içerebileceğinden ve yine de eklersek, bunun bir anlamı yoktur. bu, sorgunun yalnızca ham dize temsiline erişiriz ve onu manuel olarak ayrıştırmamız gerekir, bu kesinlikle iyi bir yaklaşım değildir. Bu kavram REST'ten GraphQL'ye pek iyi çevrilmiyor.

Dolayısıyla GraphQL sorgularını engellemek için bir tür yola ihtiyacımız var ve bu yol prisma-graphql-middleware olarak adlandırılır. Bu kitaplık, bir çözümleyici çağrılmadan önce veya sonra rastgele kod çalıştırmamızı sağlar. Kodun yeniden kullanımını ve endişelerin net bir şekilde ayrılmasını sağlayarak kod yapımızı geliştirir.

GraphQL topluluğu, bazı özel kullanım durumlarını çözen Prisma ara katman yazılımı kitaplığına dayalı bir grup harika ara katman yazılımı oluşturmuştur ve kullanıcı yetkilendirmesi için, API'miz için bir izin katmanı oluşturmamıza yardımcı olan graphql-shield adlı bir kitaplık mevcuttur.

Graphql-shield'ı kurduktan sonra, API'miz için şöyle bir izin katmanı sunabiliriz:

 import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }

Ve bu katmanı ara katman yazılımı olarak şemamıza şu şekilde uygulayabiliriz:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Burada bir kalkan nesnesi oluştururken, allowExternalErrors true olarak ayarladık, çünkü varsayılan olarak, kalkanın davranışı çözümleyiciler içinde oluşan hataları yakalayıp işlemek içindir ve bu benim örnek uygulamam için kabul edilebilir bir davranış değildi.

Yukarıdaki örnekte, API'mize yalnızca kimliği doğrulanmış kullanıcılar için erişimi kısıtladık, ancak kalkan çok esnektir ve onu kullanarak kullanıcılarımız için çok zengin bir yetkilendirme şeması uygulayabiliriz. Örneğin, örnek uygulamamızda iki rolümüz vardır: USER ve USER_MANAGER ve yalnızca USER_MANAGER rolüne sahip kullanıcılar kullanıcı yönetimi işlevini çağırabilir. Bu şu şekilde uygulanır:

 export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }

Bahsetmek istediğim bir şey daha, projemizde ara katman işlevlerinin nasıl organize edileceğidir. Şema tanımları ve çözümleyici haritalarında olduğu gibi, bunları şema başına bölmek ve ayrı dosyalarda tutmak daha iyidir, ancak şema tanımları dizilerini ve çözümleyici haritalarını kabul eden ve bizim için birleştiren Apollo sunucusunun aksine, Prisma ara katman yazılımı kitaplığı bunu yapmaz ve sadece bir ara katman harita nesnesini kabul eder, bu yüzden onları bölersek onları manuel olarak tekrar dikmek zorunda kalırız. Bu soruna yönelik çözümümü görmek için lütfen örnek projedeki ApiExplorer sınıfına bakın.

doğrulama

GraphQL SDL, kullanıcı girdisini doğrulamak için çok sınırlı işlevsellik sağlar; sadece hangi alanın gerekli, hangisinin isteğe bağlı olduğunu tanımlayabiliriz. Daha fazla doğrulama gereksinimini manuel olarak uygulamamız gerekir. Doğrulama kurallarını doğrudan çözümleyici işlevlerinde uygulayabiliriz, ancak bu işlevsellik gerçekten buraya ait değildir ve bu, GraphQL ara katman yazılımları için başka bir harika kullanım örneğidir. Örneğin, kullanıcı adının doğru bir e-posta adresi olup olmadığını, şifre girişlerinin eşleşip eşleşmediğini ve şifrenin yeterince güçlü olup olmadığını doğrulamamız gereken kullanıcı kaydı isteği giriş verilerini kullanalım. Bu şu şekilde uygulanabilir:

 import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }

Doğrulayıcılar katmanını ara katman yazılımı olarak şemamıza aşağıdaki gibi bir izin katmanıyla birlikte uygulayabiliriz:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })

N + 1 Sorgular

GraphQL API'lerinde meydana gelen ve genellikle gözden kaçan diğer bir sorun da N + 1 sorgulardır. Bu sorun, şemamızda tanımlanan türler arasında bire çok ilişkimiz olduğunda ortaya çıkar. Örneğin bunu göstermek için örnek projemizin kitap API'sini kullanalım:

 extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }

Burada, User türünün Book türü ile bir-çok ilişkisi olduğunu görüyoruz ve bu ilişki Book yaratıcı alanı olarak temsil ediliyor. Bu şema için çözümleyiciler haritası şu şekilde tanımlanır:

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

Bu API'yi kullanarak bir kitap sorgusu yürütürsek ve SQL ifadeleri günlüğüne bakarsak, şöyle bir şey göreceğiz:

 select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...

Tahmin etmesi kolaydır; yürütme sırasında, çözümleyici ilk olarak kitapların listesini döndüren kitaplar sorgusu için çağrıldı ve ardından her kitap nesnesi oluşturucu alan çözümleyicisi olarak adlandırıldı ve bu davranış N + 1 veritabanı sorgularına neden oldu. Veritabanımızı patlatmak istemiyorsak, bu tür davranışlar gerçekten harika değil.

N + 1 sorgu problemini çözmek için Facebook geliştiricileri, DataLoader adında çok ilginç bir çözüm oluşturdular ve bu çözümün BENİOKU sayfasında şu şekilde anlatıldı:

“DataLoader, toplu işleme ve önbelleğe alma yoluyla veritabanları veya web hizmetleri gibi çeşitli uzak veri kaynakları üzerinde basitleştirilmiş ve tutarlı bir API sağlamak için uygulamanızın veri alma katmanının bir parçası olarak kullanılacak genel bir yardımcı programdır”

DataLoader'ın nasıl çalıştığını anlamak çok kolay değil, bu yüzden önce yukarıda gösterilen sorunu çözen örneği görelim ve ardından arkasındaki mantığı açıklayalım.

Örnek projemizde, Creator alanı için DataLoader şu şekilde tanımlanmıştır:

 export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }

UserDataLoader'ı tanımladıktan sonra, yaratıcı alanının çözümleyicisini şu şekilde değiştirebiliriz:

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

Uygulanan değişikliklerden sonra tekrar kitap sorgusunu çalıştırıp SQL deyimleri günlüğüne bakarsak şöyle bir şey göreceğiz:

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

Burada, N + 1 veritabanı sorgularının iki sorguya indirildiğini görebiliriz; birincisi kitap listesini, ikincisi ise kitap listesinde yaratıcı olarak sunulan kullanıcıların listesini seçer. Şimdi DataLoader'ın bu sonucu nasıl elde ettiğini açıklayalım.

DataLoader'ın birincil özelliği toplu işlemdir. Tek yürütme aşaması sırasında DataLoader, tüm bireysel yükleme işlevi çağrılarının tüm farklı kimliklerini toplar ve ardından istenen tüm kimliklerle toplu işlevi çağırır. Hatırlanması gereken önemli bir nokta, DataLoaders örneklerinin yeniden kullanılamayacağıdır, toplu işlev çağrıldığında, döndürülen değerler örnekte sonsuza kadar önbelleğe alınacaktır. Bu davranış nedeniyle, her yürütme aşaması için yeni DataLoader örneği oluşturmalıyız. Bunu başarmak için, DataLoader örneğinin bir bağlam nesnesinde sunulup sunulmadığını kontrol eden ve bulunamazsa bir tane oluşturan statik bir getInstance işlevi oluşturduk. Her yürütme aşaması için yeni bir bağlam nesnesinin oluşturulduğunu ve tüm çözümleyiciler arasında paylaşıldığını unutmayın.

DataLoader'ın toplu yükleme işlevi, bir dizi farklı istenen kimliği kabul eder ve bir dizi karşılık gelen nesneye çözümlenen bir söz verir. Toplu yükleme işlevi yazarken iki önemli şeyi hatırlamalıyız:

  1. Sonuç dizisi, istenen kimlik dizisiyle aynı uzunlukta olmalıdır. Örneğin, [1, 2, 3] kimliklerini istediysek, döndürülen sonuç dizisi tam olarak üç nesne içermelidir: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Sonuç dizisindeki her dizin, istenen kimlik dizisindeki aynı dizine karşılık gelmelidir. Örneğin, istenen kimlik dizisi şu sıraya sahipse: [3, 1, 2] , o zaman döndürülen sonuç dizisi tam olarak aynı sırada nesneleri içermelidir: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

Örneğimizde, sonuçların sırasının, aşağıdaki kodla istenen kimliklerin sırasına uymasını sağlıyoruz:

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

Güvenlik

Ve son olarak, güvenlikten bahsetmek istiyorum. GraphQL ile çok esnek API'ler oluşturabilir ve kullanıcıya verilerin nasıl sorgulanacağı konusunda zengin yetenekler verebiliriz. Bu, uygulamanın müşteri tarafına oldukça fazla güç verir ve Ben Amca'nın dediği gibi, "Büyük güç, büyük sorumluluk getirir." Uygun güvenlik olmadan, kötü niyetli bir kullanıcı pahalı bir sorgu gönderebilir ve sunucumuzda bir DoS (Hizmet Reddi) saldırısına neden olabilir.

API'mizi korumak için yapabileceğimiz ilk şey, GraphQL şemasının iç gözlemini devre dışı bırakmaktır. Varsayılan olarak, bir GraphQL API sunucusu, genel olarak GraphiQL ve Apollo Playground gibi etkileşimli görsel kabuklar tarafından kullanılan tüm şemasını iç gözlem yapma yeteneğini ortaya çıkarır, ancak aynı zamanda kötü niyetli bir kullanıcının API'mize dayalı karmaşık bir sorgu oluşturması için çok yararlı olabilir. . Apollo Sunucusunu oluştururken introspection parametresini false olarak ayarlayarak bunu devre dışı bırakabiliriz:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

API'mizi korumak için yapabileceğimiz bir sonraki şey, sorgunun derinliğini sınırlamaktır. Bu, özellikle veri türlerimiz arasında döngüsel bir ilişki varsa önemlidir. Örneğin bizim örneğimizde proje tipinde Author alan kitapları, Book tipinde ise alan yazarları bulunmaktadır. Bu açıkça döngüsel bir ilişkidir ve hiçbir şey kötü niyetli kullanıcının şöyle bir sorgu yazmasını engellemez:

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

Yeterli yuvalama ile böyle bir sorgunun sunucumuzu kolayca patlatabileceği açıktır. Sorguların derinliğini sınırlamak için graphql-depth-limit adlı bir kitaplık kullanabiliriz. Yükledikten sonra, Apollo Sunucusu oluştururken aşağıdaki gibi bir derinlik kısıtlaması uygulayabiliriz:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Burada, maksimum sorgu derinliğini beş ile sınırladık.

Post Scriptum: REST'ten GraphQL'ye Geçiş İlginçtir

Bu eğitimde, GraphQL API'lerini uygulamaya başladığınızda karşılaşacağınız genel sorunları göstermeye çalıştım. Bununla birlikte, bazı kısımları çok sığ kod örnekleri sağlar ve boyutu nedeniyle tartışılan sorunun yalnızca yüzeyini çizer. Bu nedenle, daha eksiksiz kod örnekleri görmek için lütfen örnek GraphQL API projemin Git deposuna bakın: graphql-example.

Son olarak GraphQL'in gerçekten ilginç bir teknoloji olduğunu söylemek istiyorum. REST'in yerini alacak mı? Kimse bilmiyor, belki yarın hızla değişen BT dünyasında API'leri geliştirmek için daha iyi bir yaklaşım ortaya çıkacak, ancak GraphQL gerçekten kesinlikle öğrenmeye değer ilginç teknolojiler kategorisine giriyor.