Looker StudioからNotionデータベースを直接覗いてみる

こんにちは。

テックリードのTerukiです。

皆さんはLooker Studioを使っていますか？
Oh my teethでは会社のありとあらゆる場所で使われているのでそのうち紹介できたらなと思います。

今回はNotionデータベースにLooker Studioからアクセス方法を紹介。


# Looker Studioとは
本題の前に。

https://cloud.google.com/looker/docs/studio

Googleが出している無料のBIツールです。

BIツールといえばTableauやPower BI等高価なツールがありますが、Looker Studioなら無料である程度のことができます。

ダッシュボードを作るならまずは最初に使ってみても良いんじゃないかなと。

# コミュニティコネクタ

Looker Studioはデータ接続に必要なコネクタを自作できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/f72d4d1c-9a8f-4bd1-b0c6-c41ce0652625.png)

自作してしまえば理論的にはLooker Studioで連携できないデータはないのでこれを使ってNotionデータベースのデータを連携させるという算段です。

「Notion」でコミュニティコネクタを検索するといくつか引っかかるのですが、正直怪しい雰囲気があり機密データを受け渡すにはちょっと怖いところがあります。

開発者向けのドキュメントもしっかり整っていて非常に好印象です。

https://developers.google.com/looker-studio/connector?hl=ja

# Notion API

https://developers.notion.com/

NotionデータベースをAPIでアクセスするためにNotionのインテグレーションを作成します。

Workspace Ownerでないと出来ないので権限が足りない人はオーナーに依頼する必要ありです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/1afcdce2-f725-4edf-b65a-9fcb27bfeb32.png)

私は「Looker Studio Integraion」という名前で作ってみました。

# やってみる

最低限Notionに接続できるくらいのところまでをゴールとしてやってみます。

https://developers.google.com/looker-studio/connector/build?hl=ja

GASのプロジェクトを作成しますが、このコミュニティコネクタを社内に公開する場合はこのGASのプロジェクトを社内の人が見れる必要があるので共有ドライブなどにプロジェクトを作成するのが良いかなと思います。

プロジェクトを作ったら実際に動くGASを書いていきます。

事細かく解説するのもアレなのでもったいぶらずに私が書いたものをそのまま貼ります。
利用は自己責任でお願いできれば。
一部の型が未対応なので必要な方は実装お願いします:pray:

main.gs
```javascript
const connector = DataStudioApp.createCommunityConnector();
const types = connector.FieldType;
const token = PropertiesService.getScriptProperties().getProperty("NOTION_INTEGRATION_TOKEN");

function getAuthType() {
  return connector.newAuthTypeResponse().setAuthType(connector.AuthType.NONE).build();
}

function getConfig(request) {
  const config = connector.getConfig();

  config
    .newTextInput()
    .setId('database_id')
    .setName('データベースIDを入力');

  return config.build();
}

function getRecords(databaseId) {
    const url = `https://api.notion.com/v1/databases/${databaseId}/query`;

    let records = [];

    let result = null;
    let payload = '{}';
    do {
      result = JSON.parse(UrlFetchApp.fetch(url, {
        'headers': {
          'Authorization': `Bearer ${token}`,
          'Notion-Version': '2022-06-28',
          'Content-Type': 'application/json',
        },
        'method': 'post',
        'payload': payload,
      }).getContentText());
      payload = JSON.stringify({
        start_cursor: result.next_cursor
      });

      records = records.concat(result.results);
    } while (result.has_more);

    return records;
}

function getNotionSchema(databaseId) {
    const url = `https://api.notion.com/v1/databases/${databaseId}`;
    
    const result = UrlFetchApp.fetch(url, {
      'headers': {
        'Authorization': `Bearer ${token}`,
        'Notion-Version': '2022-06-28'
      }
    });

    const json = JSON.parse(result.getContentText());

    const schema = [];

    for (const key in json.properties) {
      const value = json.properties[key];

      schema[value.id.replace(/\%/g, "Z").replace(/\~/g, "Y")] = value;
    }

    return schema;
}

function getUser(userId) {
    const url = `https://api.notion.com/v1/users/${userId}`;

    const result = UrlFetchApp.fetch(url, {
      'headers': {
        'Authorization': `Bearer ${token}`,
        'Notion-Version': '2022-06-28'
      }
    });

    return JSON.parse(result.getContentText()).people[0];
}


function getSchema(request) {
  return {
    schema: getFields(request).build(),
  };
}

function getFields(request) {
  const fields = connector.getFields();
  const types = connector.FieldType;

  const typeMap = {
    'number': types.NUMBER,
    'checkbox': types.BOOLEAN,
    'status': types.TEXT,
    'rich_text': types.TEXT,
    'url': types.URL,
    'formula': types.NUMBER,
    'title': types.TEXT,
    'multi_select': types.TEXT,
    'select': types.TEXT,
    'date': types.YEAR_MONTH_DAY,
    'people': types.TEXT,
    'files': types.TEXT,
  };

  const databaseId = request.configParams['database_id'];
  const schema = getNotionSchema(databaseId);

  if (request.fields) {
    // フィールドが指定されている時
    for (const field of request.fields) {
      const column = schema[field.name];

      fields.newDimension()
        .setId(field.name)
        .setName(column.name)
        .setType(typeMap[column.type]);
    }
  } else {
    // フィールドを指定されていない時
    for (const key in schema) {
      fields.newDimension()
        .setId(key)
        .setName(schema[key].name)
        .setType(typeMap[schema[key].type]);
    }
  }

  return fields;
}

function getPropertyValue(record) {

  switch (record.type) {
    case 'number':
      return record.number;
    case 'checkbox':
      return record.checkbox;
    case 'status':
      return record.status.name;
    case 'rich_text':
      let value = '';
      for (const text of record.rich_text) {
        value += text.plain_text;
      }
      return value;
    case 'url':
      return record.url;
    case 'formula':
      return record.formula.number;
    case 'title':
      let title = '';
      for (const text of record.title) {
        title += text.plain_text;
      }
      return title;
    case 'select':
      return record.select?.name;
    case 'select':
      return record.select?.name;
    case 'people':
      if (record.people.length === 0) return '';
      return record.people[0].name;
    case 'date':
      throw new Error()
  }

  return '何かがおかしいようです';
}

function getData(request) {
  const databaseId = request.configParams['database_id'];

  const records = getRecords(databaseId);

  const fields = request.fields.map((m) => m.name);

  const rows = [];

  for (const record of records) {
    const row = [];
    const properties = [];
    for (const key in record.properties) {
      const value = record.properties[key];

      properties[value.id.replace(/\%/g, "Z").replace(/\~/g, "Y")] = value;
    }

    for (const field of fields) {
      row.push(getPropertyValue(properties[field]));
    }

    rows.push({
      'values': row
    })
  }

  return {
    schema: getFields(request).build(),
    rows: rows,
  };
}

function isAdminUser() {
  return true;
}
```

次は、GASのプロジェクトのマニフェストにLooker Studioで使うよという宣言を書きます。

プロジェクトの設定で『「appsscript.json」マニフェスト ファイルをエディタで表示する』にチェックを入れる必要があります。

具体的には下記のサンプルをコピペして一部変更して使います。

https://developers.google.com/looker-studio/connector/build?hl=ja#complete_the_project_manifest

oauthScopesの「`https://www.googleapis.com/auth/script.external_request`」が重要で、これがないとNotionのAPIにHTTPリクエストを投げることができません。

これで準備は完了です。

# 使ってみる

## Notionの権限

Looker Studioの設定に入る前に、Notionのデータベースに作成したインテグレーションがアクセスできるように権限を付けてあげる必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/b0175876-e59f-46b1-9a90-b46bd2b0fa82.png)

接続から作成したインテグレーションを選択するだけです。

全ページに権限付与するなら一番上のページに設定しておけば良いですが、読ませたいデータベースにだけ付ける運用のほうが良いと思います。

Notion APIを使うにはインテグレーショントークンが必要なので、GASのプロジェクトのスクリプトプロパティに書いておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/329642c1-ad83-4b3d-bd9f-1b0c5b47537c.png)


## Deployment IDの確認

Looker Studioのデータソースから独自コネクタを追加する際にGASのプロジェクトのDeployment IDを要求されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/fb4db128-8aad-4853-b81a-cbae58763cf7.png)

GASのエディタの右上にあるデプロイからデプロイをテストを押すとこのような画面が出てくると思うので、書いてあるデプロイIDをコピーしてLooker Studio側で入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/fd521193-446d-4cf6-8c67-a69f1e475404.png)

このように出てくればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/bbe2cf1f-f580-4d8c-92b9-c5921faef15a.png)

後はデータソースの追加で追加したNotionコネクタを選択すれば使用可能です。

データベースIDの入力を求められるので、NotionデータベースのIDを入れます。
このIDはデータベースを開いた時に表示されるURLのハッシュの部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/57d42dfa-15c7-4601-a1cc-1f0dc6a2cf5e.png)


試しにこのシンプルなNotionデータベースを読ませてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/8bdfa8d0-ed75-4290-b745-073f1fe5e17f.png)

ちゃんとディメンションとして認識していますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/8b9dd978-a387-4e15-8697-102a4acf60f0.png)


# 結構重い問題

NotionのAPIは意外と遅いので、Notionコネクタを使ったグラフをたくさん並べるとAPIのレートリミットに引っかかったり表示が遅すぎてストレスだったりすると思います。

こればっかりはどうしようもないのですが「データの抽出」コネクタを使うとストレスなく使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/589eb208-29ed-4303-a25e-0aeb1bcc264f.png)

これはデータソースとLooker Studioの間にキャッシュ機構を差し込むものです。

データの鮮度を犠牲にして中身のデータを高速なキャッシュに載せることができます。

ただし、自動更新では毎日の更新が最大の頻度になります。


# おわりに

Looker Studioはコミュニティコネクタによっていろんなものと連携させることができます。

私はこれ以外にSFDCコネクタやTrelloコネクタ、さらには自社で使用している予約システムのAPIと繋げて可視化したりしています。

結構いろいろできるのでいろいろ試してみると面白いと思います。

では。

# Oh my teethについて
[Oh my teeth](https://www.oh-my-teeth.com/?tags=qiita)では未来の歯科体験を創るために日々活動しています。

Techチームではより良いユーザー体験を提供するべく、Webフロントエンドからバックエンド、スマホアプリに機械学習モデルなど、さまざまなプロダクトを開発しています。

一緒に未来の歯科体験を創りませんか？興味がある方は是非こちらを確認してください。

https://oh.careers/

カジュアル面談も可能なので気軽に応募してみてください！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/0138b492-e02b-852e-61fa-fc065b53d285.png" width="450" />