大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【NotionAPI】【GAS】Notionのテーブルをスプレッドシートに反映

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

社内業務にてNotionのテーブルをスプレッドシートに連携させる実装をすることを頼ました。しかし、調べてみてもそのような記事は見当たらず、NotionAPIもGASも使ったことがなかったのでだいぶ苦労しました。ですので、こちらの記事にて実装した内容を共有しておきたいと思います。

使用技術

最終実装

こちらの記事では最終的に下記のような実装ができるようになります。

※ spreadsheetからNotionへのテーブルの反映機能は実装していません。

https://i.gyazo.com/fe87665556d973d3176f951932b9cd86.png

↓(転記!!!)

https://i.gyazo.com/f87457c54837d055a42330d5793fd69f.png

NotionAPIの準備

まずGASを使用してNotionからテーブルの情報を取得する方法を実装します。そのための準備としてNotionから情報を取得できるように設定します。

テーブル作成

まずはNotionで適当なテーブルページを作成します。必ず自分が管理者であるワークスペースで作成してください。

私は下記のようなページを作成しました。

https://i.gyazo.com/fe87665556d973d3176f951932b9cd86.png

今回の実装ではタイトルである「Name」カラム、マルチタグの「Tags」カラム、日付の「Date」カラムをスプレッドシートに移します。

Integration secretの作成

Integrationとは?

Integrationと聞いてピンと来ないかもしれませんが、ここでの意味はNotionAPIのAppと考えていいと思います。Slack Appみたいなものです。

下記公式手順に沿ってAPIを連携させるためのキーなどを作成していきます。

Start building with the Notion API

  1. https://www.notion.so/my-integrationsにアクセス
  2. 基本情報を入力
    1. 名前
    2. ロゴ(任意)
    3. 関連ワークスペース(先ほど作成したテーブルがあるワークスペースを選択)
    4. 機能(デフォルトのままの設定でOK)
  3. 「送信」をクリック
  4. 遷移したらトークンをコピーして控えておく

https://i.gyazo.com/dc9754788d36c2ed4b32a83f8f80a3b0.png

テーブルに連携

作成したIntegrationとテーブルを紐付けます。

  1. 先ほど作成したテーブルの右上の「Share」ボタンをクリック
  2. フォームをクリック
  3. モーダルが表示されるので先ほど作成したIntegrationを選択
  4. 「Invite」をクリック

https://i.gyazo.com/c14fce2b8a31dc5e490a9e91b630bb3b.gif

これによって作成したIntegrationからテーブルに情報を取得する準備ができました!

GASでNotionのデータにアクセス

プロジェクトの作成

適当なスプレッドシートを作成し、「拡張機能」→「Apps Script」をクリックします。

https://i.gyazo.com/8d50dc484e28bedd8246e5616d2f563e.gif

定数の定義

今回の実装をする上で値が一定であるものを定数として最初に定義しておきます。

// API用トークン
const integrationSecret = "secret_xxxxxxxxxxxxxxxxxxx";
// 書き込むスプレッドシートのID
const spreadsheetId = "xxxxxxxxxxxxxxxxxxxxxxx";
// Notionのバージョン
const notionVer = "2021-08-16";
// スプレッドシートのシート名とそれに対応するNotionのテーブルのID
const object = {
        // スプレッドシートの名前
    sheetName: " シート1",
        // NotionのテーブルのID
    dbId: "xxxxxxxxxx",
  }
// 空値
const blankValue = "";

先ほどIntegration作成時にコピーしたトークンをペースト

用意したスプレッドシートのIDを入力します。IDの調べ方は作成したシートのURLの下記該当箇所に書かれています。

https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxx/edit#gid=0
  • Notionのバージョン

こちらは使用しているNotionのバージョンを記載します。理由はヘッダーにバージョンを指定しないと下記エラーが起こってしまいAPIリクエストが失敗するからです。

Notion-Version header should be defined, instead was `undefined`.

転記先のスプレッドシートの名前をsheetNameプロパティに記載します。また、NotionのテーブルIDはNotionのテーブルのページURLのxxxxxxxxxxの部分に書かれています。

https://www.notion.so/xxxxxxxxxx?v=・・・・・・
  • 空値

共通の空値としてblankValueを定義しておきます。

Notionの情報を取得

いよいよNotionの情報を取得します。

まず今回の実装は

  1. Notionからデータを取得
  2. スプレッドシートに転記

のため、メソッドを二段階に分けるかと思います。よって前もって二段階に分ける想定で下記のようにgetNotionDatabase(Notionからデータを取得)とsetValuesToSheets(スプレッドシートに転記)を実行するnotionToSheetsメソッドを定義します。これが最終的に実行するスクリプトです。

function notionToSheets() {
  const res = getNotionDatabase(object);
  setValuesToSheets(res, object);
}

getNotionDatabaseメソッドの中身を記述します。テーブルの中身を取得するための方法は公式に書かれていました。

Start building with the Notion API

公式を参考にメソッドを定義すると以下のようになります。

function getNotionDatabase(obj) {
  const headers = {
    Authorization: `Bearer ${integrationSecret}`,
    "Content-Type": "application/json",
    "Notion-Version": `${notionVer}`,
  };
  const options = {
    headers: headers,
    method: "post",
    muteHttpExceptions: true,
  };

  let res =  UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${obj.dbId}/query`, options);
  return JSON.parse(res);
}

UrlFetchApp.fetchメソッドはGASのAPIアクセス時に使えるメソッドです。

Class UrlFetchApp | Apps Script | Google Developers

これでデータは取得はできたはずです。確認したい人はGASのデバッグ機能を使って確認してみましょう。

GASでスプレッドシートに転記

取得したデータをスプレッドシートに転記するsetValuesToSheetsメソッドを記述していきます。長いので先に書いてから上から説明していきます。

function setValuesToSheets(res, obj) {
  propertyArray = Object.keys(res.results[0].properties);

  const valueArrays = [...Array(res.results.length)].map((v, i) => {
    return propertyArray.map((prop) => {
      return translateNotionData(res.results[i].properties[prop]);
    });
  });

  // プロパティ配列を先頭に挿入
  valueArrays.unshift(propertyArray);

  // 参考リンク[https://qiita.com/cazimayaa/items/224daebe536799e5a8a2#getrangerow-column-numrows-numcolumns]
  const spreadSheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(obj.sheetName);
  const sheetRange = {
    row: 1,
    column: 1,
    numRows: valueArrays.length,
    numColumns: propertyArray.length,
  };

  const range = spreadSheet.getRange(
    sheetRange.row,
    sheetRange.column,
    sheetRange.numRows,
    sheetRange.numColumns
  );
  range.setValues(valueArrays);
}

まず一行目ですが、これは列の全てのカラムを取得しています。今回自分が作成したテーブルでいうと「Date」、「Tags」、「Name」を取得しています。

propertyArray = Object.keys(res.results[0].properties);

次の行は非常にわかりにくいです。mapメソッドが二重になっています。

const valueArrays = [...Array(res.results.length)].map((v, i) => {
  return propertyArray.map((prop) => {
    return translateNotionData(res.results[i].properties[prop]);
  });
});

まず、 [...Array(res.results.length)].map((v, i)の部分ですが、res.resultsにはNotionのテーブルのレコードごとのデータが格納されています。その数だけ繰り返し処理を行い、全てのレコードを加工しています。次にpropertyArray.map((prop)の部分ですが、後述した通りpropertyArrayはカラムとなります。そのカラムの数だけカラムを加工する繰り返し処理を行います。translateNotionData(res.results[i].properties[prop])の部分ですが、res.results[i].properties[prop]には各レコードの各カラムの値が入ります。今回の実装でいうと「Date」、「Tags」、「Name」の値が渡されています。

translateNotionDataを下記に定義します。

function translateNotionData(data) {
  if (data.type === "title") {
    return getTitleData(data);
  } else if (data.type === "multi_select") {
    return getMultiSelectData(data);
  } else if (data.type === "date") {
    return getDateData(data);
  } else {
    return blankValue;
  }
}

NotionAPIのカラムの値にはdataオブジェクトが渡されその中のtypeプロパティがそのカラムの属性を表しています。データの種類に合わせて加工しているということです。繰り返しますが今回は種類が「Date」、「Tags」、「Name」の3種類にしか対応していません。

function getTitleData(data) {
  return data.title.length ? data.title[0].plain_text.trim() : blankValue;
}
function getDateData(data) {
  if (data.date) {
    return data.date.end
      ? `${data.date.start}|${data.date.end}`
      : `${data.date.start}`;
  } else {
    return blankValue;
  }
}
function getMultiSelectData(data) {
  if (data.multi_select.length) {
    return data.multi_select
      .map((select) => {
        return select.name;
      })
      .join(", ");
  } else {
    return blankValue;
  }
}

setValuesToSheetsメソッドに戻ります。

以下の記載ではvalueArrays(テーブルの全てのレコード)にカラムの配列を先頭に挿入しています。

  // プロパティ配列を先頭に挿入
  valueArrays.unshift(propertyArray);

次にシートのどの部分に転記するかを定義します。これは記述してある参考リンクを参照いただけるとわかりやすいかと思います。

  // 参考リンク[https://qiita.com/cazimayaa/items/224daebe536799e5a8a2#getrangerow-column-numrows-numcolumns]
  const spreadSheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(obj.sheetName);
  const sheetRange = {
    row: 1,
    column: 1,
    numRows: valueArrays.length,
    numColumns: propertyArray.length,
  };

sheetRangeでは一番左上の端からレコードの数(valueArrays.length) × カラムの数(propertyArray.length)の範囲で転記させます。

いよいよ最後に転記するメソッドです。

const range = spreadSheet.getRange(
  sheetRange.row,
  sheetRange.column,
  sheetRange.numRows,
  sheetRange.numColumns
);
range.setValues(valueArrays);

定数rangeに転記する範囲を定義します。そしてrangeを使ってsetValuesメソッドを使用すると転機ができます。

https://i.gyazo.com/f87457c54837d055a42330d5793fd69f.png

最終コード

最終的にできたコードは下記になります。

// API用トークン
const integrationSecret = "secret_xxxxxxxxxxxxxxxxxxx";
// 書き込むスプレッドシートのID
const spreadsheetId = "xxxxxxxxxxxxxxxxxxxxxxx";
// Notionのバージョン
const notionVer = "2021-08-16";
// スプレッドシートのシート名とそれに対応するNotionのテーブルのID
const object = {
        // スプレッドシートの名前
    sheetName: " シート1",
        // NotionのテーブルのID
    dbId: "xxxxxxxxxx",
  }
// 空値
const blankValue = "";

function notionToSheets() {
  const res = getNotionDatabase(object);
  setValuesToSheets(res, object);
}

function getNotionDatabase(obj) {
  let data = obj.data;
  const headers = {
    Authorization: `Bearer ${integrationSecret}`,
    "Content-Type": "application/json",
    "Notion-Version": `${notionVer}`,
  };
  const options = {
    headers: headers,
    method: "post",
    muteHttpExceptions: true,
  };

  let res =  UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${obj.dbId}/query`, options);
  return JSON.parse(res);
}

function setValuesToSheets(res, obj) {
  propertyArray = Object.keys(res.results[0].properties);

  const valueArrays = [...Array(res.results.length)].map((v, i) => {
    return propertyArray.map((prop) => {
      return translateNotionData(res.results[i].properties[prop]);
    });
  });

  // プロパティ配列を先頭に挿入
  valueArrays.unshift(propertyArray);

  // 参考リンク[https://qiita.com/cazimayaa/items/224daebe536799e5a8a2#getrangerow-column-numrows-numcolumns]
  const spreadSheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(obj.sheetName);
  const sheetRange = {
    row: 1,
    column: 1,
    numRows: valueArrays.length,
    numColumns: propertyArray.length,
  };

  const range = spreadSheet.getRange(
    sheetRange.row,
    sheetRange.column,
    sheetRange.numRows,
    sheetRange.numColumns
  );
  range.setValues(valueArrays);
}

function translateNotionData(data) {
  if (data.type === "title") {
    return getTitleData(data);
  } else if (data.type === "multi_select") {
    return getMultiSelectData(data);
  } else if (data.type === "date") {
    return getDateData(data);
  } else {
    return blankValue;
  }
}

function getTitleData(data) {
  return data.title.length ? data.title[0].plain_text.trim() : blankValue;
}
function getDateData(data) {
  if (data.date) {
    return data.date.end
      ? `${data.date.start}|${data.date.end}`
      : `${data.date.start}`;
  } else {
    return blankValue;
  }
}
function getMultiSelectData(data) {
  if (data.multi_select.length) {
    return data.multi_select
      .map((select) => {
        return select.name;
      })
      .join(", ");
  } else {
    return blankValue;
  }
}

終わりに

NotionAPI、GAS共に初めて使用しましたがドキュメントは非常にわかりやすかったのですぐにAPI情報を取得することができました。

難点としてはやはりカラムのデータを加工する際にデータの種類に合わせて処理が必要なことですね。。これはどうやって対応すればいいのかわかりませんでした。よりいい方法がありましたら教えてください🙇‍♂️

以上、大ちゃんの駆け出し技術ブログでした!