Zapier Integrationを実装する魅力

こんにちは。株式会社ビットジャーニーでKibelaというサービスの開発をしているエンジニアのpockeです。
先日KibelaではZapier Integrationをベータリリースしました。つまり、Zapierを通していろんなアプリケーションとKibelaが連携できるようになりました。
https://blog.kibe.la/entry/2019/10/17/150109


Zapier Integrationの実装をしていてとても驚いたことが1つあります。それはZapier Integrationの実装がとてもシンプルかつ簡単で、developer friendlyであるということです。
実装前にはZapierについては正直名前ぐらいしか知らなかったですが、実装が終わってリリースをする頃にはすっかりファンになってしまうぐらいの魅力がありました。


この記事では、そのZapier Integrationの魅力をお伝えします。
そして少しだけ実装方法についても触れようと思います。




# 読むべきドキュメント

まず最初に、参照すべき公式ドキュメントを紹介します。

公式ドキュメントは https://platform.zapier.com/ にあるので、ここにある情報を参照していくと良いでしょう。
Zapier Integrationの仕組みや各機能の概念の説明などは一通り揃っています。



私の記事を読んでZapier Integrationを実装したくなった方は、まず https://platform.zapier.com/docs/zapier-intro から順に読み進めていくと良いでしょう。
またこの記事ではZapier Platform CLIという機能を使ってZapier Integrationの開発を行うことを前提にしています。そのため、CLIのドキュメントである https://platform.zapier.com/cli_docs/docs も役に立つでしょう。



# Zapier Integrationとは？

Zapier Integrationの魅力について説明する前に、まずはZapier Integrationとはなんなのか説明しようと思います。


Zapierに自分が作ったアプリケーションを登録したい場合、Zapier Integrationを実装することで登録できます。
つまりZapier Integrationはあなたのアプリケーションと他のアプリケーションをつなぐ役割を持っています。
Zapier Integrationを通してあなたのアプリケーションは他のアプリケーションからイベントを受け取ったり、あるいはあなたのアプリケーションからイベントを他のアプリケーションに発信できます。





## Trigger と Action


Zapier Integrationには大きくTriggerとActionという2つの機能があります。



1つ目のTriggerは、あなたのアプリケーションからイベントを発生させる機能です。
例えばKibelaでは「記事の投稿」をトリガーとしてイベントを発生させることができます。これによって、Kibelaでの記事の投稿を他のアプリケーションに通知できます。





もう1つのActionは、他のアプリケーションから発生したイベント(Trigger)によってあなたのアプリケーションで何かしらの処理を行う機能です。
例えばKibelaでは、Actionとして記事の投稿を行えます。つまり、他のアプリケーションで発生したイベントをトリガーとして、それに関連した記事を作成できます。現状ではZapier公式が提供しているSchedule Triggerを元に「週次定例のための記事を自動で作成する」といったユースケースを考えています。




# Zapier Integrationの魅力

では、Zapier Integrationの魅力を解説していきます。


## コードをアプリケーションから完全に分割できる

Zapier IntegrationはJavaScriptで実装します。
そしてJavaScriptで書いたコードはZapierが管理するAWS Lambdaの上で動きます。[^1]
つまり私達Zapier Integrationを実装する人は、Zapier関連のインフラをなにも管理する必要がないし、連携対象のアプリケーションにZapier連携のコードを同居させる必要もありません。

Kibelaは元々GraphQLを使ったパブリックなAPIを提供しているので、今回はその仕組みに乗るだけで実装できました。[^2]
実際Zapier Integrationの実装に際して、Kibela本体ではPull Request1つしか手を入れませんでした。それも足りないAPIを足しただけなので、Zapier専用のコードはKibela本体には一切入っていません。



このようにアプリケーションをきれいに分割できるのはZapier Integrationの大きな魅力の1つだと私は考えています。





## シンプルな基本APIと、応用的なAPI



次に上げられるのが、Zapier IntegrationのSDKのAPI設計の良さでしょう。
このAPIは「基本的なことはシンプルに、必要に応じて応用的なことも足せる」ようになっていて、実装がとても楽でした。


### シンプルな基本API

たとえばKibelaで実装した「記事の新規投稿を通知するTrigger」の実装を考えてみます。
これは単に「記事を表すObjectの配列を返すasync function」を実装するだけです。

たとえば次のコードはTriggerの定義として正しく動作します(固定値を返すので意味はないですが)。

```typescript
const getNewNotes = async (z: ZObject, bundle: Bundle<any>) => {
  return [
    {id: 1, title: 'Test Note 1', content: 'Hello, Kibela!', url: 'https://example.kibe.la/notes/1'},
    {id: 2, title: 'Test Note 2', content: 'Hello, Zapier!', url: 'https://example.kibe.la/notes/2'},
  ]
}
```


そして実際のコードは次のようになっています(コメントを足している以外は実際のコードそのものの一部です)。


```typescript
// 記事の取得に使うGraphQLのクエリ
const newNotesQuery = `
  query getNewNotes($groupId: ID!, $first: Int!) {
    group(id: $groupId) {
      notes(first: $first, orderBy: {field: PUBLISHED_AT, direction: DESC}) {
        edges {
          node {
            id
            title
            content
            url
            folderName
            publishedAt
            author {
              realName
              account
              url
            }
          }
        }
      }
    }
  }
`;

// 次の2引数を受け取って、配列を返すasync関数
//   * ZObject (ここではHTTPクライアントとして扱っている)
//   * Bundle (Zapier側から指定されたパラメータを抱えているオブジェクト)
const getNewNotes = async (z: ZObject, bundle: Bundle<{ groupId: RelayId }>) => {
  // kibelaClientは、自前で定義している簡易なGraphQLクライアント
  const resp = await kibelaClient.query(z, bundle, newNotesQuery, {
    first: 20,
    // inputDataにZapier側からパラメータが入ってきていて、その中のgroupIdをこのTriggerでは使用している
    groupId: bundle.inputData.groupId, 
  });
  const json = resp.json as any;
  const notes: Connection<NoteType> = json.data.group.notes;
  // GraphQLのRelay ConnectionからArrayに変換
  return arrayFromConnection(notes);
};
```



たったこれだけでよいのです。単に記事を新着順にAPIで取得して返す関数を定義するだけです。「この記事は通知済みか」と言ったことはidを見てZapier側が勝手にやってくれます。
とても簡単ですね。


### 応用的なAPI


また、応用的なこともできるようになっています。
その例としてHydrationという概念を紹介します。

先ほど次のように書きました。

> 「この記事は通知済みか」と言ったことはidを見てZapier側が勝手にやってくれます。


これはつまり、通知済みであってもなくてもあなたのアプリケーションからZapier Integration側にデータを渡さないといけないということを表します。
つまり、記事1件1件を取得するコストが高い場合、毎回すべての記事を取得するのはとてもコストがかかる操作になってしまいます。


```typescript
const newNotesQuery = `
  query getNewNotes($groupId: ID!, $first: Int!) {
    group(id: $groupId) {
      notes(first: $first, orderBy: {field: PUBLISHED_AT, direction: DESC}) {
        edges {
          node {
            id
            // このattributeを取得するコストがめちゃくちゃ大きいため
            // 毎回取得しているとサーバーの負荷がとても大きくなってしまう！
            superHeavyAttribute
            // 略
          }
        }
      }
    }
  }
`;
```


この問題はHydrationを使って解決できます。
Hydratorsを使うと、`getNewNotes`は各記事のidだけを返せば良くなります。
そして、実際に必要になった時に改めて個別に記事の詳細を取得するAPIを叩きます。
これによって`superHeavyAttribute`を評価する回数を減らすことが期待できます。


……と説明したのですが、実はKibelaではまだHydrationを使っていません。なぜならばまだこのような重いattributeを返す必要がまだ発生していないためです。
ですが、将来的に困った時にこのような解決策が提供されているのは実装の上での安心感に繋がりました。


詳しい使い方などは公式ドキュメントをご覧ください。 https://platform.zapier.com/cli_tutorials/hydration



## 公式のTypeScriptサポート

Zapier IntegrationはJavaScriptで実装すると説明しましたが、もちろんTypeScriptを使って実装することもできます。
しかも公式でTypeScriptのサポートがされています。
今回はこのexample appをベースに実装しました。 https://github.com/zapier/zapier-platform/tree/master/example-apps/typescript


もちろんZapierが提供するSDKにはTypeScriptの型定義も含まれています。
とはいえZapierのSDK自体はJavaScriptで実装されているため、型定義も若干不十分であることは否めません。ですが多少anyを補ってやるだけでよく、生でJavaScriptを書くよりはとても体験が良かったです。




## デプロイコマンド



また、Zapier Integrationはデフォルトでデプロイのためのコマンドを提供しています。
そのためZapier Integrationのコードを書き終わったら、コマンド1つでデプロイできます。

実際のコマンド定義は前に紹介したTypeScriptのexample appに書かれています。
https://github.com/zapier/zapier-platform/blob/d4c922528cf9be6f196699697fc57088ddae5591/example-apps/typescript/package.json#L10-L19



# 最後に



Zapier Integrationの魅力の紹介と、実装の簡単な説明をしました。
これを機に、Zapierを使ってみるだけではなく、Zapier Integrationを使ってみてはいかがでしょうか？


また、KibelaではZapier Integrationのベータテスタを募集しています。
https://blog.kibe.la/entry/2019/10/17/150109
興味がありましたらこちらも使っていただけたら幸いです！




[^1]: https://zapier.com/engineering/behind-the-cli/
[^2]: https://github.com/kibela/kibela-api-v1-document