Notion + GAS + Slackで、定期タスクを作成する仕組み作ったよ！(解説編)

今回は、前回投稿した「[Notion + GAS + Slackで、定期タスクを作成する仕組み作ったよ！](https://qiita.com/suica_s/items/27e1f5a992294714a4e5)」の解説編ですー。
この仕組みを作るときに気をつけたところや、技術的な解説をしてみます！
## ValidationとNotionの数式プロパティについて
今回入力に使用するGUIとなるものが、NotionDBでした。
GASでデータを受け取ってから値の検証をすると、実装が面倒です。
なので、Notionの数式プロパティを使用して、項目「間隔」の入力に合わせて、値の検証ができるようにしてみました。
```javascript:Validationの数式
if(
	prop("間隔") == "日次" and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0,
	"Valid",
	if(
		prop("間隔") == "週次" and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0 and !empty(prop("発行基準曜日")),
		"Valid",
		if(
			or(prop("間隔") == "月次",prop("間隔") == "四半期次",prop("間隔") == "半年次",prop("間隔") == "年次") and !empty(prop("設定名")) and !empty(prop("担当者")) and !empty(prop("説明書URL")) and prop("日数") >= 0 and !empty(prop("発行基準日付")),
			"Valid",
			style("InValid","b","yellow_background")
		)
	)
)
```
ところがSwitch文は使えないので、ifを入れ子にすることで実現しました。
Invalidな値は目立たせたかったので、``style("InValid","b","yellow_background")``で太字にして黄色の背景色に変更しています。
## NotionのAPI
使ってみるとわかるのですが、こいつがクセ強です。
なのでライブラリーやSDKを通しての使用をおすすめするのですが、GASでは公式で提供されていませんので、生のAPIを叩くことになります😭
### connectionの設定
APIでDatabaseを操作するには、下図のようにconnectionの設定が必要です。
実行結果で```object_not_found```と出てしまう場合は、DatabaseのIDやconnectionの設定を見直してみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112129/d975a9cb-4ffa-48a2-8194-039cf9609f71.png)
### Databaseの情報を取得
NotionDBの情報取得方法は、大まかに2つに分かれます。
- プロパティ等の設定情報を取得
    - [Retrieve a database](https://developers.notion.com/reference/retrieve-a-database)
- 保存されているページを取得
    - [Query a database](https://developers.notion.com/reference/post-database-query)

今回使用しているのは **「保存されているページを取得」** の方です。
queryとあるくらいなので、条件を指定して取得します。指定した条件は、payloadにJSON形式で指定します。
```javascript:APIコール時に設定するoption
const buildNotionApiOption_ = (method,setPayload) => {
  let buf = {
    method: method,
    contentType: "application/json",
    headers: {
      "Authorization": `Bearer ${getPropertyFromScript_("NOTION_API_TOKEN")}`,
      "Notion-Version": "2022-06-28",
    },
    muteHttpExceptions: true,
    followRedirects: false
  }
  if (setPayload !== undefined) {
    buf.payload = JSON.stringify(setPayload)
  }
  return buf
}
```
```javascript:「保存されているページを取得」APIコール
const retrieveNotionSettingDb_ = () => {
  // 定期タスク設定DBから、DB情報を取得する。
  // 発行するがtrueで、バリデーションが通っているものを対象
  const query = {
    filter: {
      and: [{
        property: "発行する",
        checkbox: {
          equals: true
        }
      },{
        property: "Validation",
        formula: {
          string: {
            equals: "Valid"
          }
        }
      }]
    }
  }
  const options = buildNotionApiOption_("get",query)
  const url = `https://api.notion.com/v1/databases/${getPropertyFromScript_("NOTION_DB_ID_SETTING")}/query`
  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  return data.results
}
```
書き方は簡単なのですが、一個一個のプロパティの指定がめんどくさいです。
なのでこの辺は**生成AIの力を借りる**と、簡単にクエリを作成できます(生成AI前提となると、この解説記事もあまり意味がないのですが😅)
フィルタークエリの書き方は、[Filter database entries](https://developers.notion.com/reference/post-database-query-filter)にあります。
### ページの追加
NotionのAPIが、クセ強である最たるものが、このページ周りの仕様です。
[Create a page](https://developers.notion.com/reference/post-page)
今回一部しか使用しておらず、最低限ページを追加したと言えるレベルです。
ここでは```generatePage_ ```にある設定内容について簡単に解説します。
- ```parent```：```database_id```に、追加先のNotionDBのIDを指定します。
- ```properties```：追加先のNotionDBに設定されているプロパティを指定します。
- ```children```：ページの内容に当たる部分です。配列で格納されており、その順でページ内のコンテンツが記載されます。block単位で、テキストやヘッダー等の各Objectを指定します。

要はこの```children```の指定が途方もなく、1ドキュメント分を指定するのに、長大なコードの記述が必要になります。
NotionDBにはテンプレート機能が存在しますが、APIにはその機能がありません。また、ページの複製もAPIでは用意されていません。
テンプレート機能外でページの生成または複製を行う場合は、この仕組みとまともに向き合う必要があります。
さすがにめんどくさすぎるので、なんとかしてほしいとも思いつつ、APIのバージョンが「2022-06-28」で止まっちゃってるので、公式開発はもうヤル気がないんでしょうか、、、？
この仕様と戦った方の記録を見つけましたので紹介しておきます。
[Notion APIで、特定の雛形のページをもとに新しいページを作成する（Googleフォーム回答をトリガーにしたケース実践）※インデントにも対応](https://qiita.com/CurryPrince/items/559b03bfc0a4c3086d62)

今回の目的は、あくまで定期タスクの発行ですので、手順説明書のページを別で用意してURLを取得し、タスクのページにはURLのみ転記する、ということで妥協した次第です。

## 日本の祝日情報をどのようにして取得するか
祝日をAPIで提供してくれているところは多いのですが、なるべく硬いところから取得したいと考えました。
Googleカレンダーが、日本の祝日カレンダーを公式で提供してくれています。
カレンダーIDは、```ja.japanese#holiday@group.v.calendar.google.com```です。
しかしこのカレンダーは、祝日以外の行事も含んでいます。なので使用するべきカレンダーIDは、```ja.japanese.official#holiday@group.v.calendar.google.com```です。
しかししかし、このカレンダーも実は、正確な祝日を取得できません😭
下記が取得結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112129/8412a942-af54-4d37-960b-8406b44131f2.png)
**12月31日大晦日が祝日扱いなのと、1月1～3日が銀行休業日という謎の日になってます。**
一旦許容範囲として実装していますが、正確ではないです。
気になる方、もしくは会社の営業日も含める場合は、別なNotionDBを設けて管理するしかなさそうです。

## constとアロー関数で書く。
今回ようやくfunctionから卒業して、constとアロー関数で書くことにしました。
ただ自分の中では、パッと見、変数の宣言と被っちゃって見づらいな、って思い、命名規則にはこだわりました。
- グローバル定数：大文字スネークケース
    - ``` RECURRENCE_INTERVAL ```
- 外部から呼び出されない関数：関数名の末尾にアンダースコア
    - ```getPropertyFromScript_```
## filterSettingsByInterval_ 
```javascript
const filterSettingsByInterval_ = (settings, interval, identifyFn, ...args) => {
  // 抽出関数の処理を共通化するためのヘルパー関数
  return settings
    .filter(setting => setting.recurrenceInterval === interval)
    .filter(setting => identifyFn(setting, ...args));
}
```
この関数は、処理の実行日と設定から算出されたタスク発行対象日とを比較して、等しければタスク生成するっていうものです。
実はここ、ChatGPTさんにレビューしてもらって、**ヘルパー関数化するといいよ！** と教えてもらったところです。
こういう書き方をしたことなかったので、メモっておきます。
これで関数を共通化できる範囲が広がりました。
### 関数の引数に関数を渡す。
```identifyFn``` には、間隔ごとで定義した日付比較関数を入れます。
呼び出す際に、別定義してある関数を引数として渡しています。
- 日次
    - ```identifyDailySettings_```
- 週次
    - ```identifyWeeklySettings_```
- それ以外の間隔
    - ```identifySettings_```
### 関数の引数を配列化する。
前述した関数の引数は、関数によって引数が変わります。
引数の長さが変わる場合は、受け取る関数側で引数をスプレッド構文化(引数の前に...を付ける)しておきます。
すると、```setting```より後ろの引数は、すべて```args```という配列に格納されます。
## Googleカレンダーのイベントは、世界標準時で保存されている。
```javascript
const retrieveHolidays_ = (nowDate) => {
  // Googleカレンダーの日本の祝日カレンダーより、祝日を取得
  // toISOString()で世界標準時になってしまうが、イベントは世界標準時で管理されているため問題はない。
  const timeMin = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate(),nowDate.getHours(),nowDate.getMinutes(),nowDate.getSeconds()).toISOString()
  const timeMax = new Date(nowDate.getFullYear()+1, nowDate.getMonth(), nowDate.getDate(),nowDate.getHours(),nowDate.getMinutes(),nowDate.getSeconds()).toISOString()
  const optionalArgs = {
    timeMin: timeMin,
    timeMax: timeMax
  }
  const holidaysResult = Calendar.Events.list("ja.japanese.official#holiday@group.v.calendar.google.com", optionalArgs)
  const holidays = new Map()
  holidaysResult.items.forEach((holiday) => holidays.set(holiday.start.date, holiday.summary))
  return holidays
}
```
今回は休日のイベントを拾うため、[Events: list](https://developers.google.com/calendar/api/v3/reference/events/list?hl=ja)を使用します。
他のパラメーターの解説はレファレンスを読んでいただくとして、```timeMax```と```timeMin```についてです。
この2つのパラメーターは、「タイムゾーン オフセットが必須の RFC3339 タイムスタンプである必要があります。」とされています。
ここで自分は遠回りなことを考えてしまいました。
``` new Date() ```で取得できるのは日本標準時です。これを```toISOString()```にして、APIのコールに使用すると、正しくイベントを取得できるのか、、、？:thinking:
**結論大丈夫でした。**
- toISOString()は、世界標準時に直して出力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112129/a1ade2ee-acfa-4d3b-ae29-7f2a6a7472ca.png)
- イベントは、世界標準時で保存されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112129/c3435b89-7381-4467-a6fe-6389458ce009.png)

実はイベントのstartとendの時刻に、何が設定されているかまでは調べきれなかったのですが、検証した範囲だと世界標準時で検索するで良さそうです。

## 生成AIによるコードレビュー
生成AIによるコードレビューは、単なる指摘ではなく、工夫して書いた部分もちゃんと褒めてくれるので、すごくレビューっぽいです。これすごくおすすめです！
今回使用したのは、**ChatGPTのo3-mini**、いわゆる推論モードです。
速度優先というより、しっかり考えて精度の高い回答をしてくれるので、コードの意図もしっかり汲み取りつつ指摘をもらえるので、大変賢かったです。
なお下げる意図は無いのですが、、、GeminiAdvanced(1.5Pro相当)も使用してみましたが、結構辛めでレビューと言うよりダメ出しでした。
コードの意図を汲み取れない箇所もあり、まだコードレビューには使えないかなーという感想です。



以上！
今回の成果としては、NotionAPIと真面目に向き合ったことと、生成AIによるコードレビューで新たな書き方を発見できたことでした。特に生成AIによるコードレビューは、一人でコード書く人間にとってはすごく有用です、すごくおすすめ！！