PipedreamとAirtableで緩く作るQRコード受付（の基盤サイド）

前回[「Pipedreamの紹介」と「ワークフローの作成例」](https://qiita.com/attakei/items/1455e56634db57e479b9)を書きました。
今回は、これを使って社内向けに作ったちょっとしたサービスの話をします。

## 何を作ったの？

社内のオフライン交流会向けに「QRコードによる受付」を作ってみました。
大まかには次のコンポーネントに分かれるのですが、今回話題の対象となるのは1.と2.です。

1. **Airtable上での、出席者・出席記録管理**
1. **Pipereamを使った、出席者用QRコード表示画面**
1. QRコード表示画面を案内するための、メール送信用ツール
1. QRコードをかざして出席記録を取るための、受付アプリ

![html-with-qrcode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/57c2345a-cd54-c64c-5172-1709be7cfc23.png)

## Airtableって何？

まずは、データ管理の基盤として使っているAirtableの話題から始めます。

https://www.airtable.com/home/toolkit

Airtableは、データストアサービスの一種です。
内部データ的には（おそらく）複数のテーブルを管理するKVSなのですが、データのI/Fとしてテーブル状のビューやフォームなどを提供する機能をもっています。
これらの機能の他にもシンプルなREST APIも持っているため、外部連携のしやすさも合わせてノーコード系のバックエンドとして採用されることも多いみたいです。

<details><summary>見え方などをちょっとだけ紹介</summary>

テーブルビューの様子。IDは出ていませんが、その他の属性を柔軟に指定することが出来ます。
- 左から、メールアドレス、日時、文字列、フラグ
- 属性を後付でいつでも追加可能

![airtable-grid-view.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/4351fc04-b61f-7c06-3957-a936d926539f.png)

APIもプレイグラウンドごと提供してくれています。

テーブルのまとまりをBaseとして管理しているのですが、BaseをURLとして組み込んだ状態で表示してくれているので、試すのも楽です。
ちなみに、公式ではNode用のAPIクライアントを公開しており、JSであれば簡単に利用できます。（実際にAPIプレイグラウンドではJavaScript用のタブでコード例を参照できます）

![airtable-apiview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/2980ab57-51e7-dc8f-c844-d6ccf81580aa.png)


</details>

### どんなデータを管理するか

今回のサイトのためにAirtable上にどのようなデータ定義をしたかを紹介します。
※スクリーンショットの加工が手間なので、テーブルベースでの記述です。

#### `events`テーブル

イベントそのものを管理するのが目的です。
単発ならベタ書きすればよいため特に必要ないのですが、もう1回ぐらいは需要がありそうなのでデータとして管理することにしています。

<table>
  <tr><th>キー</th><th>役割</th></tr>
  <tr><td>key</td><td>対象イベントのテーブル名</td></tr>
  <tr><td>title</td><td>イベント名</td></tr>
</table>

#### `{event-key}`テーブル

イベントごとの来訪者を管理します。
前述の受付用QRコード画面自体に必要な情報の他に、コードの送信先やコードの利用タイミングなどを管理しています。

<table>
  <tr><th>キー</th><th>役割</th></tr>
  <tr><td>name</td><td>来訪者名</td></tr>
  <tr><td>arrived</td><td>受付日時</td></tr>
  <tr><td>note</td><td>出欠用のメモ書き</td></tr>
  <tr><td>email</td><td>URL送付先のメールアドレス</td></tr>
  <tr><td>create_posted</td><td>メール送信済みフラグ（バッチ処理用）</td></tr>
</table>

ちなみに、スキーマ管理の観点では「来訪者情報を全イベントで集約する」ほうがもちろん楽です。
ただ、Airtable上ではテーブル間のリレーションシップなどが無いのと、Pipedream上での処理を簡略化させたいのもあり、イベント単位でテーブルを分割することにしています。

## ワークフロー紹介

冒頭に書いた通り、画面表示を含めて全てPipedream上で実装しています。実際にどんな実装をしたかの解説をしていきます。
前回の記事にもある通り、事前にどんな振る舞いをするかを整理していきます。

- いつ：トリガー
    - HTTPリクエストが来た時
- 何を：アクション
    - クエリパラメータをもとに、イベント情報を取得する
    - クエリパラメータとイベント情報をもとに、来訪者情報を取得する
    - イベント情報と来訪者情報をもとに、QRコード付きのHTMLをレスポンスする

### 今回のトリガー

「HTTPリクエスト」という条件を満たす必要があります。
Pipedreamでは、トリガーとして機能するためのHTTPリクエストのエンドポイントを作成することが出来ます。
作成すると `https://{ランダム文字列}.m.pipedream.net` というURLが作成され、実際にブラウザ等でリクエストすることでイベントデータの取得をすることが出来ます。

![pipedream-http-endpoint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/a92c2fb4-05cf-6d4c-7bc3-e688606f9801.png)

画像は、HTTPトリガーを作成して、実際にクエリ付きでリクエストした様子です。
例によって、クエリパラメータはオブジェクトとして後続アクションに引き継がれていきます。これを利用することで、データ取得を進めていきます。

### 今回のアクション1: Airtableからデータ取得する

Airtable連携はPipedreamが提供してくれており、必要な情報を渡すことでレコードに対するCRUD処理を自由に行なえます。

![pipedream-airtable-getrecord.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/81bbba51-a231-4b49-177a-a0bc41549d27.png)

画面は単一レコードを取得するアクションの表示。
Base,対象テーブル,対象レコードIDを指定すると、「レコードがあればレコードを引き継いで後続処理へ」「レコードがなければ処理失敗」という振る舞いをします。

### 今回のアクション2: QRコードを含むHTMLを生成する

まるごとコードを載せます。

<details><summary>※長いのでクリックして展開※</summary>

```js
import QRCode from "qrcode-svg";

// To use previous step data, pass the `steps` object to the run() function
export default defineComponent({
  async run({ steps, $ }) {
    const src = {
      content: steps.trigger.event.query.uid,
      xmlDeclaration: false,
    };
    const svg = new QRCode(src).svg();
    const html = `<!DOCTYPE html>
      <html>
        <head>
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
          <style>
            .content dd {margin-left: 0;}
          </style>
        </head>
        <body>
          <section class="section has-text-centered">
            <h1 class="title">
              受付用QRコード
            </h1>
            <div class="content">
              ${svg}
              <dl>
                <dt>イベント</dt>
                <dd>${steps.get_event.$return_value.fields.title}</dd>
                <dt>参加者名</dt>
                <dd>${steps.get_attend.$return_value.fields.name}</dd>
              </dl>
            </div>
          </section>
        </body>
      </html>`;

    await $.respond({
      status: 200,
      headers: {
        "content-type": "text/html",
      },
      body: html,
    })
  },
})
```

</details>

`defineComponent()`->`run()`が呼び出されるので、この中で必要な処理をしたうえで`$.respond()`を実行することでHTTPレスポンスを返すことが出来ます。簡単でした。

内部でやっていることは、「ユーザーIDをもとにQRコードをSVGで作成」して「来訪者情報などと一緒にQRコードを埋め込んだHTMLを作成」するというものになっています。

QRコードのために[qrcode-svg](https://npmjs.com/package/qrcode-svg)というライブラリを用いていますが、Pipedreamの仕様として勝手にインストールしてくれるので、非常に気楽に利用できます。

また、HTMLはただのテキスト+SVGもだたのテキストなので、素のHTMLのテンプレートリテラルに変数（SVG,イベント名,来訪者名）を何も考えずに埋め込むだけで完結します。CSSフレームワークとしてBlumaを使用しているため、最低限の体裁は整えられました。

### 出来上がったワークフローがこちら

![workflow-overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/19203b31-a496-fcb0-1e6c-988b9982df84.png)

「トリガー（提供） -> サービス連携（提供） -> サービス連携（提供） -> レスポンス（自作）」という、Pipedreamらしい実装で簡易的な動的ページが作成できました。

## 切り捨てたこと

「パラメータ不正を含めた、バックエンド処理エラー時の画面」を一切実装していません。
例えば、クエリパラメータが無い状態でのアクセスをしたり、favicon相当のURLへのアクセスをすると、以下のようなメッセージがポツンと表示されます。

![error-response.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6805/949f73e9-0b39-c068-9643-b42aa5593599.png)

これは、Pipedreamのワークフローが「アクションの処理に失敗した際には即時で動きを停止」してしまい、レスポンスの整形にたどり着かないことに起因します。
現状ではPipedreamに二又の分岐処理が実現できないため、アプリ仕様としてエラー画面を出すのは少々困難です。

ただし、以下のよう状況であることと実装時間のバランスを考えて今回は見なかったことにしました。

- 処理失敗時の画面を見ても、表示される情報が皆無
    - 「キーの組み合わせではデータが存在しない」ぐらいまで
- URLはグローバルだけど社内メンバーに渡すだけ
    - QRコードを表示するような画面ではあり得る実装でもある
- アクションをまたいだコンテキスト処理の実装が面倒そう
    - データストアがあるため、出来なくはないが手間

## 振り返り

「達成目的を最小化する」ことを前提にはしたものの、Pipedreamの素振りとしては必要な機能がきちんと揃った動的ページ生成が実現できました。
この規模感だと、Firebase（Functions + Firestore）でも十分実現可能なのですが、「最終的に結果をエクスポートする」ことの障壁を考慮して今回の形式に落ち着いています。

条件次第ではFaaSより一段階楽に作れるため、「選択肢」としての価値は十分高いかなと感じました。