授權方式(Auhorization): CC-BY 4.0

GraphQL

graphQL 是 facebook 為解決 REST 問題所提出的查詢語言。一共有五大特點 Hierarchical, Product‐centric, Strong‐typing, Client‐specified queries, Introspective。這邊都不贅述,有興趣請自行去翻 facebook 的介紹。

實作

目前有看到幾套實作:

  1. graphql-js 官方在 nodejs 上面的實作版本,也是稍後要介紹的。
  2. graphql-go go 上面的實作。
  3. sangria scala 上面的實作。

我的觀點

graphQL 解決了幾個 REST 問題。

  1. 只用 http POST 方法。
  2. 省去 Endpoint 定義,不用煩惱 table 之間的 relation 要用怎樣的 endpoint 結構。
  3. 只有 query 跟 mutation,剩下都是彈性自訂的 function name。
  4. 可以合併幾個 REST 查詢變成同一個 query。
  5. 導入型別系統,確保資料型別都是正確。
  6. 其他還有可以做 validation, introspection 等。

實戰

這邊搭配 express, mongodb, graphql,以一個 api.ly 的角度切入解釋。

query and response

我們可能在資料庫是一張 Table (Collection) 存放草案,另一個存放立委。

可能的資料形式

const bills = [{ 
    id: "1537L17367",
    abstract: "....",
    type: "xxx",
    sponsors: ["legislator_id_1"],
    cosponsors: ["legislator_id_2", "legislator_id_3", "legislator_id_4"]
}];
  
const legislators = [{
    id: "legislator_id_1",
    name: "林國正"
}, ... ];

假設我們希望拿到全部草案的摘要、提案人、連署人,可能會下這樣的 query 。
這邊有一些注意的地方是,graphql 不論 query 或 mutation 都是字串。
主要是 bills 對應到後端的 resolve function name,另外空白、換行都是一樣。
因為 bills 是 function 所以後面可以加 argument。像是 bills(type: 'xxx')。
這次主力介紹 nested query ,argument 的方式可以參考其他文章,有必要下次再講。

GraphQL Query

query bills {
  id
  abstract
  sponsors {
    name
  }
  cosponsors {
    name
  }
}

希望拿到的 Response JSON

data: {
  bills: [
    {
      id: "1537L17367",
      abstract: "本院委員林國正等20人,鑑於促進....",
      sponsors: [
        {name: "林國正"}
      ],
      cosponsors: [    
        {name: "孫大千"},
        {name: "盧嘉辰"}, 
        {name: "王育敏"},
            ...
      ]
    }
  ]
}

主要是建立一個 /graphql endpoint 使用 post method,另外把 db connection 傳到 graphql global context 裡面去。

app.js
import express from 'express';
import mongodb from 'mongodb';
import bodyParser from 'body-parser';
import config from './config';
import {Schema} from './schema';

mongodb.MongoClient.connect(config, {uri_decode_auth: true}, (err, db) => {
  if(err) throw Error('Mongo connect failed');
  let app = express();
  app.locals.db = db;
  app.post('/graphql', bodyParser.text(), (req, res, next) => {
    let db = req.app.locals.db;
  
    graphql(Schema, req.body, {db})
    .then((result) => {
      res.send(result);
      return next();
    }, (error) => {
      res.status(500).send(error.message);
    });
  });
});

主要建立一個 schema ,並且將 query 應該要 resolve 的 function name 做 mapping 。

schema.js
// here import GraphQL types

import {
  graphql,
  GraphQLEnumType,
  GraphQLFloat,
  GraphQLInt,
  GraphQLList,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
} from 'graphql';

import { bills } from './query';

export const Schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: () => {
      return {
        bills
      };
    },
  })
});

主要建立一個回傳所有 bill 的 query function ,使用 GraphQLList 跟自定義的 billType 串接。

query.js
// here import GraphQL types


import {
  billType
} from './type';

export const bills = {
  type: new GraphQLList(billType),

  resolve: ({db}, args, info) => {

    return new Promise((resolve, reject) => {
      db.collection('bills')
        .find()
        .toArray((err, result) => {
      
          if (err) return reject(err);
          return resolve(result);
      });
    });
  }
};

這邊類似過去 ORM 的 model。特別注意的是 nested query 是利用 field resolve 完成,像是 mongoose 的 virtual method。主要是第一個 argument 是先把該 bill 值 resolve 給你,再讓你決定怎麼串出結果。
然後第三個 argument 裡面可以拿到 global context,於是就可以愉快的用 db connection 。
不用這種作法,若是使用 Mongoose,可以在這邊 import {Bill, Legislator} from 'models'的做法,在裡面做 ORM的動作。

type.js
// here import GraphQL types

// 這邊是對應資料庫的 schema 定義,!代表不能是空值。

// type Legislator = {

//    id: String!,

//    name: String  

// }

// type Bill = {

//    id: String!,

//    abstract: String,

//    type: String,

//    sponsors: [String],

//    cosponsors: [String],

// }

//

export const legislatorType = new GraphQLObjectType({
  name: 'Legislator',
  fields: {
    id: { type: GraphQLString },
    name: { type: GraphQLString }
    .... 
  }
});

export const billType = new GraphQLObjectType({
  name: 'Bill',
  fields: {
    id: { type: GraphQLString },
    abstract: { type: GraphQLString },
    type: { type: GraphQLString },
    cosponsors: { 
      type: GraphQLList(legislatorType),
      resolve: (bill, args, {rootValue: {db}}) => {
        const cosponsors = bill.cosponsors.map( id => { return {id}; });
        return new Promise((resolve, reject) => {
          db.collection('legislators')
            .find({ $or: cosponsors })
            .toArray((err, result) => {
   
              if (err) return reject(err);
              return resolve(result);
          });
        });
      }
    },
    sponsors: { 
      type: GraphQLList(legislatorType),
      resolve: (bill, args, {rootValue: {db}}) => {
        const sponsors = bill.sponsors.map( id => { return {id}; });
        return new Promise((resolve, reject) => {
          db.collection('legislators')
            .find({ $or: sponsors })
            .toArray((err, result) => {
   
              if (err) return reject(err);
              return resolve(result);
          });
        });
      } 
    } // end of sponsors

  }
});

然後 graphql 支援 promise ,在 sync 和 async 上都不用太費力就可以完成。
關於 mutation 和其他的 validation 和 introspection 等到下一篇再來解釋了。

參考文章
GraphQL Introduction
GraphQL
Your Fist GraphQL Server
GraphQL Overview

最後感謝我強大的同事 @czchen 建立良好的撰寫 graphql 環境!