GraphQL の歴史を探求してきました。それから Query もある程度はかけるようになりました。Schema も作れますね。やっと、これらすべてを活用した GraphQL サービスを作る準備が整いました。GraphQL のサービスは、かなり広い範囲の様々な技術を使って実装することができますが、我々は JavaScript を使うこととします。ここで紹介するテクニックは、普遍的なものなので、言語やフレームワークによって実装の詳細が異なってはいますが、全体的なアーキテクチャは GraphQL で共通です。

You explored the history. You wrote some queries. You created a schema. Now you’re ready to create a fully functioning GraphQL service. This can be done with a range of different technologies, but we’re going to use JavaScript. The techniques that are shared here are fairly universal, so even if the implementation details differ, the overall architecture will be similar no matter which language or framework you choose.

JavaScript 以外の言語のための GraphQL のサーバー側用ライブラリについては、GraphQL.org で確認することができます。

If you are interested in server libraries for other languages, you can check out the many that exist at GraphQL.org.

When the GraphQL spec was released in 2015, it focused on a clear explanation of the query language and type system. It intentionally left details about server implementation more vague to allow developers from a variety of language backgrounds to use what was comfortable for them. The team at Facebook did provide a reference implementation that they built in JavaScript called GraphQL.js. Along with this, they released express-graphql, a simple way to create a GraphQL server with Express, and notably, the first library to help developers accomplish this task.

After our exploration of JavaScript implementations of GraphQL servers, we’ve chosen to use Apollo Server, an open-source solution from the Apollo team. Apollo Server is fairly simple to set up and offers an array of production-ready features including subscription support, file uploads, a data source API for quickly hooking up existing services, and Apollo Engine integration out of the box. It also includes GraphQL Playground for writing queries directly in the browser.

雑なインストールコマンド yarn add -D apollo-server graphql nodemon

あく

Resolvers

今まで GraphQL についてみてきた中で、Query についてかなり時間を割いてきましたね。Schema は Query の operation を定義します。operation というのはつまり、クライアントがどのような query を発行することが許可されていて、それからどのような型が関連づけられているか、ということが定義されています。Schema はこれらの要件については定義をしますが、実際にデータを取得する作業は行いません。その役割は resolver にあります。

In our discussion of GraphQL so far, we’ve focused a lot on queries. A schema defines the query operations that clients are allowed to make and also how different types are related. A schema describes the data requirements but doesn’t perform the work of getting that data. That work is handled by resolvers. A

Resolver は、指定された field のための、データを返す関数です。Resolver 用の関数は、schema によって定義された型と形状のデータを返します。Resolver は非同期処理をさせることができるので、REST API やデータベースから fetch をしたり、もしくはそれらに対して情報の更新をすることができます。

resolver is a function that returns data for a particular field. Resolver functions return data in the type and shape specified by the schema. Resolvers can be asynchronous and can fetch or update data from a REST API, database, or any other service.

では root query がどのようなものなのか、軽くみていきましょう。index.js ファイルをプロジェクトのルートに配置し、Query 内に totalPhotos フィールドを追加しましょう。

Let’s take a look at what a resolver might look like for our root query. In our index.js file at the root of the project, let’s add the totalPhotos field to the Query:

index.js
const typeDefs = `
  type Query{ 
    totalPhotos: Int! 
  } 
`;

const resolvers = {
  Query: {
    totalPhotos: () => 42
  }
};

const { ApolloServer } = require("apollo-server");

const typeDefs = type Query{ totalPhotos: Int! };

const resolvers = { Query: { totalPhotos: () => 42 } };

const server = new ApolloServer({ typeDefs, resolvers });

server .listen() .then(({ url }) => console.log(GraphQL Service running on ${url}));

Roote Resolver

とりあえずこれで mutation までできた。

const { ApolloServer } = require("apollo-server");

const typeDefs = `
  type Query{ 
    totalPhotos: Int! 
  }
   
  type Mutation{
    postPhoto(name:String! description:String):Boolean!
  }
`;

var photos = [];

const resolvers = {
  Query: {
    totalPhotos: () => photos.length
  },

  Mutation: {
    postPhoto(parent, args) {
      photos.push(args)
      return true
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

server
  .listen()
  .then(({ url }) => console.log(` GraphQL Service running on ${url} `));

Type Resolver

GraphQL の query, mutation, subcsription が実行されると、query と同じ形状の結果を返します。今までは scalar type の値、つまり integers, strings, Booleans を返していましたが、Resolver は object を返すこともできます。

When a GraphQL query, mutation, or subscription is executed, it returns a result that is the same shape of the query. We’ve seen how resolvers can return scalar type values like integers, strings, and Booleans, but resolvers can also return objects.

今作成中の photo app に、Photo type を作成し、allPhoto query filed を追加します。この field がPhoto object のリストを返すようにしましょう。

For our photo app, let’s create a Photo type and an allPhotos query field that will return a list of Photo objects:

Photo object と allPhotos query を type definitions に追加したので、それを resolver のほうにも反映させる必要があります。postPhoto mutation は Photo type の形状をしたデータを返すようにしましょう。allPhotos に query を発行すると、Photo type の形状をしたオブジェクトを要素として持つ配列を返すようにします。

Because we’ve added the Photo object and the allPhotos query to our type definitions, we need reflect these adjustments in the resolvers. The postPhoto mutation needs to return data in the shape of the Photo type. The query allPhotos needs to return a list of objects that have the same shape as the Photo type:

Photo type は ID を必要とするので、ID を保持する変数を作成しました。postPhoto resolver の中で、この変数を一つ増やして ID として使用します。受け取った arg 変数は、photo オブジェクトの name と descript field のために使用されます。しかし ID も必要ですよね。identifier や timestamp のような変数を生成するのは一般的にいって server 側の役割です。ですので新しい photo オブジェクトを resolver の中で作成する際に、ID filed を追加し、name と description field は args からうけとって、新しい photo オブジェクトを作成するために使用します。

Because the Photo type requires an ID, we created a variable to store the ID. In the postPhoto resolver, we will generate IDs by incrementing this value. The args variable provides the name and description fields for the photo, but we also need an ID. It is typically up to the server to create variables like identifiers and timestamps. So, when we create a new photo object in the postPhoto resolver, we add the ID field and spread the name and description fields from args into our new photo object.

以前は Boolean を返していましたが、mutation は photo type の形状の object を返すようにしましょう。この object は、新しく生成された ID フィールドと、クエリ情報から受け取った name と description フィールドで構成されます。さらに resolver の postPhoto mutation の中では、photo object を photos array に追加する作業重行います。追加されるオブジェクトは schema で定義した Photo type の形状に適合していますので、結果として作成される photos array は、allPhotos query で定義した型になっているので (つまり photo type のオブジェックトが要素として入っている配列になっているので)、allPhotos によって、この配列全体を返すことができます。

type Query{
  allPhotos: [Photo!]! 
}

Instead of returning a Boolean, the mutation returns an object that matches the shape of the Photo type. This object is constructed with the generated ID and the name and description fields that were passed in with data. Additionally, the postPhoto mutation adds photo objects to the photos array. These objects match the shape of the Photo type that we defined in our schema, so we can return the entire array of photos from the allPhotos query.

実装した postPhoto が正常に動いているかを確認するために、mutation を実行してデータを変更してみましょう。返される値は Photo という型を持ったものなので、それに必要な部分を mutation のリクエストに追加します。(訳注: Photo type のオブジェクトが返ってくるので、例えば id, name といったそれに対応した絞り込み用のフィールドを mutaiton 実行のクエリに追加するということ)

GraphQL
mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}
Query Object に以下を入力
{ 
  "name": "sample photo A", 
  "description": "A sample photo for our dataset" 
}

To verify that postPhoto is working correctly, we can adjust the mutation. Because Photo is a type, we need to add a selection set to our mutation:

mutation によって何枚か photo が追加されたら、以下の allPhotos query を発行して、追加された Photo object によって構成される array が返ってきていることを確認しましょう。

After adding a few photos via mutations, the following allPhotos query should return an array of all of the Photo objects added:

mutation 結果確認の query
query listPhotos {
  allPhotos {
    id
    name
    description
  }
}

ここまでの実装結果

ここまでの実装結果
const { ApolloServer } = require("apollo-server");

const typeDefs = `
  # 1.photo type の定義を追加する
  type Photo{
    id: ID!
    url: String!
    name: String!
    description: String
  }
  
  # 2.allPhotos field から Photo を返す
  type Query{ 
    totalPhotos: Int!
    allPhotos: [Photo!]! 
  }
   
  # 3. mutation で新しく追加した photo を返す
  type Mutation{
    postPhoto(name:String! description:String):Photo!
  }
`;

// id 用の変数
var _id = 0;
var photos = [];

const resolvers = {
  Query: {
    totalPhotos: () => photos.length,
    allPhotos: () => photos
  },

  Mutation: {
    postPhoto(parent, args) {
      // 新しい photo を作成する。その際に新しい id を生成する。
      var newPhoto = {
        id: _id++,
        ...args
      };
      photos.push(newPhoto);
      
      // 作成した新しい photo を返す
      return newPhoto;
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

server
  .listen()
  .then(({ url }) => console.log(` GraphQL Service running on ${url} `));

We also added a non-nullable url field to our photo schema. What happens when we add a url to our selection set?

When url is added to our query’s selection set, an error is displayed: Cannot return null for non-nullable field Photo.url. We do not add a url field in the dataset. We do not need to store URLs, because they can be automatically generated. Each field in our schema can map to a resolver. All we need to do is add a Photo object to our list of resolvers and define the fields that we want to map to functions. In this case, we want to use a function to help us resolve URLs:

Because we are going to use a resolver for photo URLs, we’ve added a Photo object to our resolvers. This Photo resolver added to the root is called a trivial resolver. Trivial resolvers are added to the top level of the resolvers object, but they are not required. We have the option to create custom resolvers for the Photo object using a trivial resolver. If you do not specify a trivial resolver, GraphQL will fall back to a default resolver that returns a property as the same name as the field.

When we select a photo’s url in our query, the corresponding resolver function is invoked. The first argument sent to resolvers is always the parent object. In this case, the parent represents the current Photo object that is being resolved. We’re assuming here that our service handles only JPEG images. Those images are named by their photo ID and can be found on the http:// yoursite.com/ img/ route. Because the parent is the photo, we can obtain the photo’s ID through this argument and use it to automatically generate a URL for the current photo.

まとめ

GraphQL schema を定義することで、作成するアプリケーションに必要な data の要件を規定しました。Resolver を使うことで、これらの要件によって必要なデータを、強力にそして柔軟に提供することができます。この強力さと柔軟性は、関数によってもたらされます。関数は非同期にすることができ、返す値は scalar type の値でも oject 値をも返すことができますし、様々なソースから取得したデータを返すこともできます。Resolver は単なる関数で、GraphQL schema で定義した filed は全て、対応する resolver に紐づけられます。

When we define a GraphQL schema, we describe the data requirements of our application. With resolvers, we can powerfully and flexibly fulfill those requirements. Functions give us this power and flexibility. Functions can be asynchronous, can return scalar types and return objects, and can return data from various sources. Resolvers are just functions, and every field in our GraphQL schema can map to a resolver.