【コピペでOK！】続・Notion APIで、特定の雛形のページをもとに新しいページを作成する方法@Google Apps Script

# はじめに
こちらの記事は、以下の記事の続きです。
以下の記事では順を追って処理の説明等をしているので、詳細が気になる方は（長いですが）まず以下をご確認ください。

https://qiita.com/CurryPrince/items/559b03bfc0a4c3086d62

この記事では処理の詳細等の説明は特に行いません。
上記記事で作成したコードを一般化して使いやすくしたものを掲載することが目的です。

# ソースコード
このプロジェクトは、`global_variables.gs`, `page_properties.gs`, `functions.gs`の3つのファイルから構成されています。

|ファイル名|説明|
|:---|:---|
|global_variables.gs|参照するページやデータベース等の共通変数を格納しているファイルです。お使いのNotionにあわせて内容を設定してください。|
|page_properties.gs|新規作成するページのプロパティを設定するためのファイルです。実際に使用する雛形にあわせて編集してください。|
|functions.gs|各種関数を格納しているファイルです。編集は不要です。|

```JavaScript:global_variables.gs
//こちらとあわせて、NotionのAPIトークンを
//"notionApiToken"としてプロパティストアに保存すること。

//データベースをフルページで開いた際のURLの、?v=の前の32文字
const NOTION_DB_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

//テンプレートページ（フルページ）のURLの最後の32文字。ハイフンがある場合はハイフンより後ろの部分
const NOTION_TEMPLETE_PAGE_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

//Notionにデータを送る際には不要なプロパティ
const UNNECESSARY_PROPERTIES = [
  "object",
  "id",
  "created_time",
  "last_edited_time",
  "created_by",
  "last_edited_by",
  "annotations",
  "archived",
  "plain_text",
  "href",
  "type"
];
```

```JavaScript:page_properties.gs
//テンプレートページのプロパティを取得
//まずはこの関数を実行し、テンプレートページのプロパティ構造を確認する
function checkSampleNotionPageProperties(){
  const pageId = NOTION_TEMPLETE_PAGE_ID;
  let response = fetchNotionPageProperties(pageId);
  response = deletePropertiesFromObj(response, UNNECESSARY_PROPERTIES);
  console.log(JSON.stringify(response, null, 4));
}

//上記で確認したプロパティ構造をもとに、ページ内容にあわせて書き換える
function setNewNotionPageProperties(properties){
  const obj = {
    "parent": {
      "database_id": NOTION_DB_ID
    },
    "properties": {
      "電話番号": {
        "phone_number": properties.tel
      },
      "メールアドレス": {
        "email": properties.mail
      },
      "利用開始日": {
        "date": {
          "start": properties.start_date,
          "end": null,
          "time_zone": null
        }
      },
      "Name": {
        "title": [{
          "text": {
            "content": properties.name
          }
        }]
      }
    }
  }

  return obj;
}
```

```JavaScript:functions.gs
//指定した内容でNotion APIにリクエストを送る
function requestNotionApi(requestUrl, method, payload = null){
  const token = PropertiesService.getScriptProperties().getProperty("notionApiToken");

  const headers = {
    "Authorization" : "Bearer " + token,
    "Content-Type" : "application/json",
    "Notion-Version" : "2022-06-28" //最新バージョンは https://developers.notion.com/reference/versioning から確認
  };

  let requestOptions = {
    "method" : method,
    "headers" : headers,
    "muteHttpExceptions" : true
  }

  if(payload != null){
    requestOptions.payload = JSON.stringify(payload);
  }

  try {
    const response = JSON.parse(UrlFetchApp.fetch(requestUrl, requestOptions));
    return response;
  } catch(e) {
    console.log(e.message);
  }
}

//指定したページのプロパティを取得
function fetchNotionPageProperties(pageId){
  const requestUrl = "https://api.notion.com/v1/pages/" + pageId;
  const response = requestNotionApi(requestUrl, "get");
  return response;
}

//指定したページのBlock object（ページの本文部分）を取得
//callRecursivelyをtrueにすると、children含めて再帰的にすべて取得
function fetchNotionPageBlocks(blockId, callRecursively=false){
  const requestUrl = "https://api.notion.com/v1/blocks/" + blockId + "/children?page_size=100";
  const response = requestNotionApi(requestUrl, "get");

  let results = response.results;

  if(callRecursively){
    for(const key of Object.keys(results)){
      if(results[key].has_children){
        results[key].children = fetchNotionPageBlocks(results[key].id, true);
      }
    }
  }

  return results;
}

//テンプレートページのBlock object（ページの本文部分）を取得
function fetchSampleNotionPageBlocks(callRecursively=false){
  const pageId = NOTION_TEMPLETE_PAGE_ID;
  const response = fetchNotionPageBlocks(pageId, callRecursively);
  return response;
}

//新しいNotionページとして利用するオブジェクトの作成
function createNotionPageObj(properties){
  //プロパティ部分の定義
  let page = setNewNotionPageProperties(properties);

  //Block Object（本文部分）の定義
  let blocks = fetchSampleNotionPageBlocks();
  deletePropertiesFromObj(blocks, UNNECESSARY_PROPERTIES);

  //結合
  page.children = blocks;

  return page;
}

//オブジェクトをもとに、新しいNotionページを作成
function createNewNotionPage(notionPageObj) {
  const requestUrl = "https://api.notion.com/v1/pages";
  const response = requestNotionApi(requestUrl, "post", notionPageObj);
  return response;
}

//テンプレートページをもとに、新規ページを作成
function duplicateTempletePageWithNewProperties(properties){
  const notionPageObj = createNotionPageObj(properties);
  const newlyCreatedPageProperties = createNewNotionPage(notionPageObj);
  
  //APIで新たに作成したNotionページのブロック要素（本文部分）を取得
  //この段階ではインデントされた行（子孫要素）は取得できない
  const pageBlocks = fetchNotionPageBlocks(newlyCreatedPageProperties.id);

  //テンプレートとなるページを、子孫要素を含めて再帰的に取得
  const templetePageBlocks = fetchSampleNotionPageBlocks(true);

  //テンプレートページに合わせて、新たに作成したページに子孫要素を追加
  cloneChildrenFromTemplete(pageBlocks, templetePageBlocks);
}

//指定したblockIdにchildrenArrayの内容の子要素を追加する
function appendChildren(blockId, childrenArray){
  const requestUrl = "https://api.notion.com/v1/blocks/" + blockId + "/children";
  const payload = {
    "children" : childrenArray
  };
  const response = requestNotionApi(requestUrl, "patch", payload);
  return response;
}

//指定したページに対して、テンプレートページを照応させながら子要素を複製する
function cloneChildrenFromTemplete(pageBlocks, templetePageBlocks){
  for(const key of Object.keys(pageBlocks)){
    const blockId = pageBlocks[key].id;
    if(templetePageBlocks[key].has_children){
      //オブジェクトはデフォルトだと参照渡しされるため、元データを変えてしまわないように複製したものを用意する
      let childrenTemplete = duplicateObject(templetePageBlocks[key].children);
      
      //不要なプロパティの削除
      childrenTemplete = deletePropertiesFromObj(childrenTemplete, UNNECESSARY_PROPERTIES);
      
      //孫となるchildrenを含むオブジェクトを投げるとエラーになるため、削除
      childrenTemplete = deletePropertiesFromObj(childrenTemplete, ["children"]);

      const response = appendChildren(blockId, childrenTemplete);

      //再帰的に関数を実行し、孫要素がある場合には同様の処理を行う
      cloneChildrenFromTemplete(response.results, templetePageBlocks[key].children);
    }
  }
}

//引数がオブジェクトまたは配列であればtrueを返す
function isObject(value) {
  return value !== null && typeof value === 'object';
}

//オブジェクトをディープコピーする
function duplicateObject(obj){
  const retObj = JSON.parse(JSON.stringify(obj));
  return retObj;
}

//オブジェクト（子孫含む）から、指定したプロパティの一覧をすべて削除
function deletePropertiesFromObj(obj, properties){
  for(const property of properties){
    delete obj[property];
  }
  for(const key of Object.keys(obj)){
    if(isObject(obj[key])){
      deletePropertiesFromObj(obj[key], properties);
    }
  }
  return obj;
}
```

# 設定が必要な項目
## スクリプトプロパティ
エディタの「プロジェクトの設定」画面から、`notionApiToken`のスクリプトプロパティを追加してください。
こちらはNotionで発行した`secret_`から始まる`Internal Integration Token`です。
取得方法については以下を参考にしてください。

https://qiita.com/CurryPrince/items/559b03bfc0a4c3086d62?fbclid=IwAR2I67DQrYIhBcuaFV-ORlMC-OTcNVLBcXvejehmJZIwcM3Vqyu6mOcchI8#notion-api%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99

![スクリーンショット 2023-02-25 19.23.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2956515/73659580-5ad6-b8f7-3692-d3deb0d7a34b.png)

## global_variables.gs
`NOTION_DB_ID`には、操作対象となるNotionのデータベースをフルページで開いた際のURLの`?v=`の前の32文字を入力してください。

`NOTION_TEMPLETE_PAGE_ID`には、雛形となるページをフルページで開いた際のURLの最後の32文字を入力してください。ハイフンがある場合は、ハイフンより後ろの部分が対象となります。

## page_properties.gs
ここが少し複雑です。まずはエディタから`checkSampleNotionPageProperties`関数を実行し、ログを確認してください。このログが、雛形ページのプロパティの設定内容となっています。
そのログ内容にあわせて、`setNewNotionPageProperties`関数内の`obj`の宣言内容を調整してください。

調整方法は、以下のとおりです。
1. `checkSampleNotionPageProperties`のログの`properties`の中身をコピーし、`obj`の`properties`にペーストする。
2. 変数として用いたいプロパティの値を、`properties.xxx`と書き換える。電話番号であれば`properties.tel`のような形式。※内容は引数として渡すオブジェクトにあわせて調整してください
3. 新規カード作成時に定義不要なプロパティを削除する。

# 使い方
設定したいプロパティを持ったオブジェクトを引数として、`duplicateTempletePageWithNewProperties`関数を呼び出してください。

# 使用例
以前の記事で紹介したGoogleフォームの回答を渡してあげるケースだと、以下のようなコードになります。

```JavaScript:main.gs
function onFormSubmit(){
  let response = {};
  for(let i = 1; i <= 10; i++){
    try{
      response = getNewestFormResponse();
      break;
    } catch(error) {
      console.log(error.message);
      if(i === 10){
        console.log("フォームの回答取得に失敗したため、スクリプトを終了します");
        return;
      }
      Utilities.sleep(10000);
    }
  }

  const parsedFormResponse = parseResponse(response);
  duplicateTempletePageWithNewProperties(parsedFormResponse);
}

function getNewestFormResponse(){
  const form = FormApp.getActiveForm();
  let yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);
  const responses = form.getResponses(yesterday);
  const newestResponse = responses.pop();
  return newestResponse;
}

function parseResponse(e){
  //すべての質問と回答を取得する
  const itemResponses = e.getItemResponses();

  //質問と回答から各要素を取得してオブジェクトを再生成。
  //選択式の質問で無回答だとインデックスがスキップされるため、インデックスは使用しない。
  let parsedResponses = {};

  //メールアドレスのみ取得方法が特殊なので注意（フォーム設定で「メールアドレスを収集する」にチェックを入れた場合）
  parsedResponses.mail = e.getRespondentEmail();

  //フォームの質問と回答を1件ずつチェックして、オブジェクトに収納
  for(let i = 0; i < itemResponses.length; i++){
    const title = itemResponses[i].getItem().getTitle();
    const response = itemResponses[i].getResponse();
    
    switch (title) {
      case "お名前":
        parsedResponses.name = response;
        break;
      case "携帯電話番号":
        parsedResponses.tel = response;
        break;
      case "利用開始日":
        parsedResponses.start_date = response;
        break;
    }
  }

  return parsedResponses;
}
```