TypeScript
angular
GraphQL
apollo
TypeORM

はじめに

みなさん、GraphQLってますか?
1年前は2017年はGraphQLが爆発的に普及するはずだ!って思っていたのですが、1年前とそれほど状況は変わっていない感じもします。

2018年をGraphQLの年とすべく、Angular と GraphQLを組合せて使ってみたいと思います。

GraphQLの実装はいろいろありますが、活発に開発が進められている Apollo を使ってみます。

GraphQL

GraphQLはサーバとのデータのやり取りなどで使用するRESTの置き換えになるような存在です。
主な機能として query, mutate, subscription の操作が行えます。

| query | データを取得 |
| mutation | データを変更 |
| subscription | データの購読 |

サンプル構成

Angular と GraphQL の組合せを全体的に試せるように、フルスタックな構成にしたいと思います。

Angular-Full-Stack っていうちょうどいいテンプレートプロジェクトがあるのでこちらをベースにします。
https://github.com/DavideViolante/Angular-Full-Stack

Angular-Full-Stack は WebサーバのExpress、 DBサーバにMongoDB、 そしてAngular CLIベースのクライントWebが組み合わさっています。

とりあえず動かす

Angular-Full-Stackをクローンして、ひとまず動かしてみます。

事前に、Node.js MongoDB はインストールしておきます。

** 5.0に変更 **
先日、Angular 5.1がリリースされて、Angular-Full-Stackも 5.1になっているのですが、GraphQLのモジュールのインストールが上手くいかないので、取り急ぎ5.0の頃のを使用します。

コミットログ から 5.1に上がる前のコミットIDを確認して、ブランチを切ります。

git checkout -b 5.0 20959cbd0679a34e115eb799ebc47b3086b32383

あとは angular cliのインストールと、gitクローン, npm installを実行すると準備万端です。

npm i -g @angular/cli
git clone https://github.com/DavideViolante/Angular-Full-Stack.git sample
cd sample
npm install

クローンしたgitリポジトリの中で、 npm run dev と実行するとExpressの起動や、Angularのコンパイルを行い、ブラウザに表示されます。

npm run dev

動いたら Cats って猫ちゃんの情報を管理できるページなんかが用意されています。

スクリーンショット 2017-12-19 15.35.39.png

このCatsの機能はいわゆるREST APIを使って実装されているのですが、同じような機能をGraphQLで作る方法をまとめていきたいと思います。

Apollo Server とかインストール

まずはサーバ側の準備から。
公式ドキュメントのこの辺りの抜粋です

npm で必要なモジュールをインストールします。

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

エンドポイントの設定

ブラウザ等からGraphQLにアクセスする為のエンドポイントと、開発に便利なGraphiQLというコンソールアプリケーションのエンドポイントを設定します。

公式ドキュメントを参考にすると、server/app.ts にコードを書いていく感じになりますが、ゴチャゴチャするのでserver/graphql/schema.tsというファイルを作って、まずはこちらにGraphQLの型情報などを定義していきます。

server/graphql/schema.ts
import { makeExecutableSchema } from 'graphql-tools';

// Some fake data
const books = [
  {
    title: 'Harry Potter and the Sorcerer\'s stone',
    author: 'J.K. Rowling'
  },
  {
    title: 'Jurassic Park',
    author: 'Michael Crichton'
  }
];

// The GraphQL schema in string form
const typeDefs = `
  type Query { books: [Book] }
  type Book { title: String, author: String }
`;

// The resolvers
const resolvers = {
  Query: { books: () => books }
};

// Put together a schema
export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

server/app.ts で、このスキーマを使用してエンドポイントを組み込んでいきます。

server/app.ts
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
import { schema } from './graphql/schema';

// The GraphQL endpoint
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));

// GraphiQL, a visual editor for queries
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));

npm run dev すると http://localhost:4200/ が表示されますが、Expressは3000番ポートで待受ています。run dev した時は proxy機能を使用しているので、 /graphql, /graphiqlもプロキシするように proxy.conf.json を変更します。

proxy.conf.json
{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  },
  "/graphql": {
    "target": "http://localhost:3000",
    "secure": false
  },
  "/graphiql": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

npm run dev で実行して http://localhost:4200/graphiql をブラウザで開いて GraphiQLの画面が表示されればバッチリです。

スクリーンショット 2017-12-19 17.12.40.png

GraphiQLは、スキーマの構成を確認できたり、graphqlを手軽に実行できてとても便利です。

猫スキーマ

現在のスキーマはApolloのドキュメントのBooksの内容になっているので、Apollo-Full-Stackが用意している猫のデータを扱えるようにスキーマを変更します。

graphql/schema.ts
import Cat from '../models/cat';
import { makeExecutableSchema } from 'graphql-tools';

// The GraphQL schema in string form
const typeDefs = `
  type Query {
     cats: [Cat]
  }

  type Mutation {
    addCat(name: String!, age: Int!, weight: Float!): Cat
    updateCat(id: String!, name: String!, age: Int!, weight: Float!): Cat
    removeCat(id: String!): Cat
  }

  type Cat { _id: String, name: String, age: Int, weight: Float}
`;

// The resolvers
const resolvers = {
  Query: { 
    cats: async () => allCats(),
    cat: async (_, args) => findCat(args.id),
  },
  Mutation: {
    addCat: async (_, args) => insertCat(args.name, args.age, args.weight),
    updateCat: async (_, args) => updateCat(args.id, args.name, args.age, args.weight),
    removeCat: async (_, args) => removeCat(args.id)
  }
};

// Put together a schema
export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

async function allCats() {
  return await Cat.find();
}

async function findCat(id: string) {
  return await Cat.findOne({ _id: id });
}

async function insertCat(name: string, age: number, weight: number) {
  let cat = new Cat();
  cat.name = name;
  cat.age = age;
  cat.weight = weight;
  return await cat.save();
}

async function updateCat(id: string, name: string, age: number, weight: number) {
  let cat = await findCat(id);
  console.log(cat);
  cat.name = name;
  cat.age = age;
  cat.weight = weight;
  console.log(cat);
  return await cat.save();
}

async function removeCat(id: string) {
  return await Cat.findOneAndRemove({_id: id});
}

GraphiQL http://localhost:4200/graphiql を使って、CRUD操作ができることを確認します。

GraphQL クライアント

サーバ側の準備が整ったので、AngularのクライアントアプリにGraphQLクライアントモジュールの設定を行います。

公式ドキュメントのこのあたり https://www.apollographql.com/docs/angular/basics/setup.html

npm install apollo-angular apollo-angular-link-http apollo-client apollo-cache-inmemory graphql-tag  --save

Apolloクライアントモジュールの設定

AppModuleでApolloクライアントの設定を行います。

client/app/app.module.ts
// Apolloクライアントなどのインポート
import { HttpClientModule } from '@angular/common/http';
import { ApolloModule, Apollo } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
...

@NgModule({
...
  imports: [
    RoutingModule,
    SharedModule,
    HttpClientModule, // provides HttpClient for HttpLink
    ApolloModule,
    HttpLinkModule
  ],

// AppModuleのコンストラクタでApolloクライアントの設定
export class AppModule {
  constructor(
    apollo: Apollo,
    httpLink: HttpLink
  ) {
    apollo.create({
      link: httpLink.create({}),
      cache: new InMemoryCache()
    });
  }
 }

Apolloクライアントの準備ができたので、GraphQLを使ってみます。
Angular-Full-Stackで用意されている、CatsComponentと同等の機能を作ってみようと思います。
CatsComponentをコピーして、CatsGraphqlComponentを作ってみます。

コンポーネントを追加するので、AppModuleへの定義とルーティングの設定を追加します。

client/app/app.module.ts
// インポート追加
import { CatsGraphqlComponent } from './cats-graphql/cats-graphql.component';
...

  // declarationsにコンポーネントを追加
  declarations: [
    ...
    CatsGraphqlComponent,
  ],
client/app/routing.module.ts
// コンポーネントをインポート
import { CatsGraphqlComponent } from './cats-graphql/cats-graphql.component';
...
  // /cats-graphqlで表示されるようルーティング追加
  { path: 'cats-graphql', component: CatsGraphqlComponent },
   ...

/cats-graphqlへのリンクを追加

client/app/app.component.html
      <a routerLink="/cats-graphql" class="nav-item nav-link" routerLinkActive="active">
        <i class="fa fa-list"></i> Cats GQL
      </a>

CatsGraphqlComponentの実装

GraphQLを利用するように変更を行います。

CRUD操作をしているgetCats, addCat, editCat, deleteCatを変更します。

編集を行う際は注意が必要で、getCatsで取得するデータは、Observableを通して渡ってきて、その際にimmutable(変更不可)になっています。

選択したCatオブジェクトをフォームとデータバインドしても、immutableなのでフォームの値が反映されません。
そこで編集を開始する enableEditing の中で、オブジェクトのクローンを生成するようにしています。

client/app/cats-graphql/cats-graphql.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';

import { Apollo } from 'apollo-angular';
import { ApolloQueryResult } from 'apollo-client';
import { Observable } from 'rxjs/Observable';
import { QueryRef } from 'apollo-angular/QueryRef';
import gql from 'graphql-tag';

import { ToastComponent } from '../shared/toast/toast.component';
import { Cat } from '../shared/models/cat.model';

const AllCatsQuery = gql`
query {
  cats {
    _id
    name
    age
    weight
  }
}`;

const CatAdd = gql`
mutation CatAdd($name: String!, $age: Int!, $weight: Float!) {
  addCat(name: $name, age: $age, weight: $weight) {
    _id, name, age, weight
  }
}`;

const CatUpdate = gql`
mutation CatUpdate($_id: String!, $name: String!, $age: Int!, $weight: Float!) {
  updateCat(id: $_id, name: $name, age: $age, weight: $weight) {
    _id, name, age, weight
  }
}`;

const CatRemove = gql`
mutation CatRemove($_id: String!) {
  removeCat(id: $_id) {
    _id, name, age, weight
  }
}`;

@Component({
  selector: 'app-cats-graphql',
  templateUrl: './cats-graphql.component.html',
  styleUrls: ['./cats-graphql.component.scss']
})
export class CatsGraphqlComponent implements OnInit {
  cat = new Cat();
  cats: Observable<Cat>;
  catsQueryRef: QueryRef<any>;
  isLoading = true;
  isEditing = false;

  addCatForm: FormGroup;
  name = new FormControl('', Validators.required);
  age = new FormControl('', Validators.required);
  weight = new FormControl('', Validators.required);

  constructor(private apollo: Apollo,
              private formBuilder: FormBuilder,
              public toast: ToastComponent) { }

  ngOnInit() {
    this.getCats();
    this.addCatForm = this.formBuilder.group({
      name: this.name,
      age: this.age,
      weight: this.weight
    });
  }

  getCats() {
    this.catsQueryRef = this.apollo.watchQuery<any>({
      query: AllCatsQuery
    });
    this.catsQueryRef.valueChanges
      .subscribe(({data}) => {
        this.isLoading = data.loading;
        this.cats = data.cats;
      });
  }

  addCat() {
    this.apollo.mutate({
      mutation: CatAdd,
      variables: this.addCatForm.value
    }).subscribe(({ data }) => {
      console.log('got data', data);
      this.catsQueryRef.refetch();
      this.addCatForm.reset();
      this.toast.setMessage('item added successfully.', 'success');
    }, (error) => {
      console.log('there was an error sending the query', error);
    });
  }

  enableEditing(cat: Cat) {
    this.isEditing = true;
    // observableで取得したオブジェクトはimmutableになっているのでクローン生成
    this.cat = JSON.parse(JSON.stringify(cat));
  }

  cancelEditing() {
    this.isEditing = false;
    this.cat = new Cat();
    this.toast.setMessage('item editing cancelled.', 'warning');
    // reload the cats to reset the editing
    this.getCats();
  }

  editCat(cat: Cat) {
    this.apollo.mutate({
      mutation: CatUpdate,
      variables: cat
    }).subscribe(({ data }) => {
      console.log('got data', data);
      this.catsQueryRef.refetch();
      this.isEditing = false;
      this.toast.setMessage('item edited successfully.', 'success');
    }, (error) => {
      console.log('there was an error sending the query', error);
    });
  }

  deleteCat(cat: Cat) {
    if (window.confirm('Are you sure you want to permanently delete this item?')) {
      this.apollo.mutate({
        mutation: CatRemove,
        variables: {_id: cat._id}
      }).subscribe(({ data }) => {
        this.toast.setMessage('item deleted successfully.', 'success');
        this.catsQueryRef.refetch();
      }, (error) => {
        console.log('there was an error sending the query', error);
      });
    }
  }
}

最後に

簡単なサンプルを通してGraphQLを使ってみました。
今回はデータ構造がシンプルなので、関連するオブジェクトもなくGraphQLを使うメリットはあまり感じられなかったかもしれないです。

githubにも上げてみました。
https://github.com/kponda/Angular-Full-Stack-GraphQL-Sample/commit/80886910e6a2d7ba1e27b6feb3824e6432ff80b7

今回は扱っていませんが、データの変更をサーバからプッシュするSubscriptionの機能なんかも面白いです。
2018年 GraphQLが盛り上がるように、ちょっと試してみるきっかけになれば幸いです。