notion API で notion DB の新規 page をテンプレートから作成する

この記事は [SUPER STUDIO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/super-studio) の1日目の記事になります。

# 概要

notion API から notionDB に用意されたテンプレートから page を作成しようと思ったのですが、単純に page を複製するような API が用意されていなかったので、こう実装して実現したという話です。
他にもやりようはあると思うので、もしもっと良いやり方があったら教えて下さい。

# やりたかったこと

slack のスレッドから、特定のスタンプが押されたら notion の page が作成されるという動作を、slack api、notion api と、API Gateway、Lambda などを利用してで実装しようとしました。
page は notionDB で管理したいので、その notionDB のデフォルトのテンプレートから page を新規作成したいです。これを notion API でやろうとしたときに、単純に template から page を作成する API が notion に用意されておらず、実装を考える必要がありました。
notion はデータ構造が単純なようなので、実際に API をいじってみると理解はしやすいですが、割と原始的な関数しか用意されていないため、やりたいことを形にするには色々と組み合わせて上げる必要があるようです。

# notion の仕様の確認

## notion API の page 作成の構造

notion は page を階層で管理することができますが、内部的にはその page がどの parent に所属しているかどうかという、とても単純な情報で階層が管理されています。
notionDB の page は、parent としてデータベースが指定されている page であり、通常の page の下に page が作られている親子関係と、設定上大きな違いはありません。

## notionDB のテンプレートの扱い

notionDB は WebUI から新しい page を作成するとき、元になる template を指定できます。
この template は notion の内部的には、通常の page と違いがありません。そのため、template から page を複製するということは、単純に template と名がついた page を複製するということと違いがありません。

## notion の page の中身を複製する

notion の page は大きく2つの情報で構成されています。propertiy と block です。property はその page についている属性情報、block は page の中身のようなイメージです。
page の中身についてですが、こちらも構造としては単純で、block の parent として page が指定されていると、その block は page の中身になります。

# 実装

## コードの構成

基本的には以下のようにAPIを実行します。

1. 新しい page を作成する [Create a page](https://developers.notion.com/reference/post-page)
1. template の page から、現在の page の中身を取得する [Retrieve block children](https://developers.notion.com/reference/get-block-children)
1. (1) で作成した新しい page に (2) で取得した blocks を append する [Append block children](https://developers.notion.com/reference/patch-block-children)
1. (2) で blocks の中に children を持っている要素があったら取得する [Retrieve block children](https://developers.notion.com/reference/get-block-children)
1. (4) で取得した blocks を append する [Append block children](https://developers.notion.com/reference/patch-block-children)
1. (4)(5)を blocks の children がなくなるまで繰り返す

少し複雑なのは、page の中身の block も階層構造を取りますが、template の page からは1階層分の深さの blocks しか取得できません。テンプレートの文書の構成が block の階層深く書かれていると、すべて取得するためには各 block に対して、children の情報を再帰的に取得する必要があります。
また、取得した階層のまま新しい page に append したいわけですが、新しい page に作成された block は、元の block と ID が変わっているため、新しい page に append する際には parent の id を変換して上げる必要があります。

## コード例

荒くてすみませんが、python で以下のようにコードを書いて動くことを確認しています。
（Lambda のコードから抜粋のため、実際に動かす際はうまいこと調整してください）

```notionlib.py
import requests
import os

NOTION_API_KEY = os.environ['NOTION_API_KEY']
DATABASE_ID = os.environ['DATABASE_ID']
HEADERS = {
    'Notion-Version': '2022-06-28',
    'Authorization': 'Bearer {}'.format(NOTION_API_KEY),
    'Content-Type': 'application/json',
}

def get_blocks(id:str):
    url = 'https://api.notion.com/v1/blocks/{}/children'.format(id)
    response = requests.get(url, headers=HEADERS)
    return response.json().get('results')

def update_blocks(id:str, blocks:dict):
    url = 'https://api.notion.com/v1/blocks/{}/children'.format(id)
    data = { 'children': blocks }
    response = requests.patch(url, headers=HEADERS, json=data)
    return response.json().get('results')

def copy_template_blocks(from_id:str, to_id:str):
    blocks = get_blocks(from_id)
    blocks_new = update_blocks(to_id, filter_blocks(blocks))
    l = []
    for i in range(len(blocks)):
        if blocks[i].get('has_children'):
            l.append({ 'from': blocks[i]['id'], 'to': blocks_new[i]['id'] })
    return l

def filter_blocks(blocks):
    def filter_block(block):
        j = { 'object': 'block' }
        j['type'] = block['type']
        j[j['type']] = block[j['type']]
        return j
    return [ filter_block(block) for block in blocks ]

def create_page(...):
    url = 'https://api.notion.com/v1/pages'
    data = {
        'parent': { 'database_id': DATABASE_ID },
        # ここにpage作成時に必要なpropertiesを指定する
    }

    response = requests.post(url, headers=HEADERS, json=data)
    return response.json().get('url')
```

```lambda_function.py(部分抜粋)
import notionlib

# notion
TEMPLATE_PAGE_ID = os.environ['TEMPLATE_PAGE_ID']

(略)

def notion(...):
    newpage_url = notionlib.create_page(...)
    newpage_id = newpage_url.split('-')[-1].split('/')[-1]
    print('[info] newpage_id: {}'.format(newpage_id))  # debug

    children = notionlib.copy_template_blocks(TEMPLATE_PAGE_ID, newpage_id)
    while len(children) > 0:
        new_children = []
        for c in children:
            new_children.extend(notionlib.copy_template_blocks(c['from'], c['to']))
        children = new_children

    return newpage_url

(略)

# main関数
def lambda_handler(event, context):
    (略)

    # create notion page
    notion_url = notion(...)
    message = '以下のnotionのpageを作成しました。\n{}'.format(notion_url)

(略)

```