Notion API による simple table の更新
◆ Notion API 雑感
-
bulk update と bulk delete ができないので、変更に時間がかかる
変更のために block を1つずつ触っていくことになるのだが、1秒に3回まで?のアクセスリミットに引っかからないようにしなければならず、変更速度が遅い\\
\\ -
update block はタイプの変更が不可 + append blockは位置指定が不可 = どうしようもない
『「block のタイプの変更」を伴う内容の差し替え』には、向いていない
ブロックの親子関係を利用することで、一応、対策ができる。
ポイント:操作したいブロックを、callout などの children を持てる block の子要素にする
問題:API を叩く前に notion 上で準備する必要があるので、面倒
-
bulk delete
callout などの親要素に対して delete block を行う。親ブロックごと消去すると、子ブロックの数に関係なく1回の API 呼び出しで消去ができる\\
\\ -
位置を維持した差し替え
更新後の内容のブロックを、callout などの親要素の children として追加し、 元のブロックを削除する。親が動かないので、要素の位置も変化しない
「親 -- 中間 -- 子」の3層構造にすると、上の2つを組み合わせることで bluk update が比較的簡単に行える[1]
-
notion 上で、更新したい複数のブロックを中間要素の子要素にする
-
API を使い、親要素に対して、新しいブロックを「中間要素その2」として追加する
-
更新後の内容のブロックを、中間要素その2を親要素として、新規に追加する[2]
-
中間要素のブロックを削除する (ことで更新前のブロックをまるごと削除する)
\\
\\
問題:ブロック自体が別のものになり、元のものの変更履歴等が継承されない
-
bluk update自体は API の正式な機能として追加するという話が
が外れた際にあった気がする。が、見つからない ↩︎\beta -
table block の場合は「table 追加 → table row 追加」のように2回APIを呼ぶ必要はなく、「table row を children として持った table 追加」で済む。他のタイプの場合、追加するブロックが children を持っているとエラーになるっぽい。エラーメッセージは
body.children[0].children should be not present, instead was [{"object":"block","type":"bulleted_list_item","bullete...]
だった (が他にもメッセージの内容はあるので、これが原因かどうかは不明) ↩︎
↑のアンサー的なものを探っていた過程でいろいろ触った結果
◆リンクの話の内容:ざっくり
-
notion は KaTex が使えるので、
\color{色名やhexコード}{色を付けたい文字}
命令がデータベースのプロパティ(や simple table のセル)で使える- 例えば
\color{#942343}{██████}
は、 という表示になる\color{#942343}{██████}
- 例えば
-
データベースのプロパティの1つにカラーコードを設定し、また別のプロパティに Tex コードで色を表示させることで、↓のような状態にしたい
-
手動でやるのは辛いので、hexコードを入力するだけで texコードを自動的に挿入させる方法はないだろうか?
◆結論:たぶん無い
- formula を使えば texコードを生成することはできる
- しかし、texコードを『数式』として評価させることができない (と思う)
texコードが KaTex で処理されているとき、内部的には、そのテキストが equation block として扱われていると思われる。これは rich text object の一種らしい。
これに対し、fomula はあくまでも fomula であって rich text object ではない。
したがって equation block として評価させることはできず、tex コードを生成できても texコードとしては機能しない、という状態[1]のようである。
-
pythonの
f"{fomula の生成物}"
のような文字列フォーマットが可能ならできそうなんだけど ↩︎
で、思いついたのが、「hexコードから texコード文字列を生成し、それを equation block として API を使って追加すればよいのでは?」ってこと。
データベースでやるのは色々としんどそうだったので、simple table でやってみることにした。
やりたいこと
-
↓このような simple table が notion 上に存在しているとする
日本語名 英語名 カラーコード 色 ブラック black #000000 ミッドナイトブルー midnight blue #001e43 インクブルー ink blue #003f8e ボトルグリーン bottle green #004d25 クロムグリーン chrome green #00533f -
このテーブルの内容を API を使って取得して、それぞれの hexコードから対応する texコードを生成する
-
「色」列のそれぞれのセルに、API を使って生成した texコードを数式として挿入する
もう少し具体的な「やること」
- 当該 simple table の id を取得して、
notion.blocks.children.list(テーブル)
で各行のデータを取得する - 各行のデータから hexコードを取り出し、texコードを生成する
- 生成した texコードを expression として持つ、
type="equation"
のオブジェクトを作る - 各行のデータにおいて、「色」列に対応するセルのデータを上で作成したオブジェクトに差し替える (今回の場合、空セルでデータが
[]
になっているのでここにオブジェクトを入れる) - それぞれの行において、差し替えたデータを与えて
notion.blocks.update(行)
を行う - あるいは、差し替えたデータを新規行として
notion.blocks.children.append(テーブル)
でまとめて追加し、古い行を一個づつ消す - あるいは、 差し替えたデータを行として持つ、新しい simple table を
notion.blocks.children.append(ページ)
で追加し、古いテーブルを削除する
更新方法の比較
- それぞれの列で
notion.blocks.update(行)
:
✅ 副作用なし ❌ 全ての行に追加されるまで時間がかかる\\[7pt]
\\ -
notion.blocks.children.append(テーブル)
でまとめて追加:
❌ 行の記録の連続性が途切れる ✅ 追加自体は一瞬だが、⚠ 掃除に手間がかかる\\[7pt]
\\ - 新しい simple table を
notion.blocks.children.append(ページ)
で追加:
❌ テーブルの記録の連続性が途切れる ✅ 一瞬で終わるが、⚠ 場所がページ末尾に
些細な追加情報
-
\color{色}{████}
より\colorbox{色}{日本語空白×5個くらい}
のほうが綺麗な表示になるunicode Full block \color{#942343}{█████} 日本語空白 \colorbox{#942343}{ }
方法:テーブルに更新済みのデータを新規行として一括追加し、古い行を順次削除する
- なぜか update が上手く機能しないので、こっちの方法で行う
- update でやる場合は、追加処理を削除して promise の直列処理の内容を更新に変える
// notion 上で copy link to block を行って id を取得し、テーブルの id を得る
const target_url = "https://www.notion.so/~~ページのID~~#~~テーブルのID~~";
const [page_id, table_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
// セルデータを作成する関数:数式はテキストと異なる prop にする必要がある
function set_celldata_obj(is_eq, text){
const annot_props = { "bold": false, "italic": false, "strikethrough": false,
"underline": false, "code": false, "color": "default"
};
let obj = new Object({"annotations": annot_props, "plain_text": text, "href": null});
if (is_eq) {
obj.type = "equation";
obj.equation = {"expression": text}
} else {
obj.type = "text";
obj.text = {"content": text, "link": null}
};
return obj }
// セルデータの更新: blocks.children.list(~) でデータを取ってきて差し替える
const new_table_rows = await notion.blocks.children.list({ block_id:table_id }).then( response => {
// 必要な prop だけ取り出す
const org_table_rows = response.results.map(x => { const {id, type, table_row} = x; return {id, type, table_row} });
// カラーコードを見つける → texコードを作成 → [] のセルデータを texコードのものに差し替え
// 空セルでは cell=[]≠[{Object}] であり、 cell[0].plain_text を適用できないので処理を分ける
const table_rows = org_table_rows.slice(1).map( row => {
const color_code = row.table_row.cells.map( cell => (cell.length) ? cell[0].plain_text : "" ).find(text => text.includes("#"));
const new_cell = set_celldata_obj(true, `\\colorbox{${color_code}}{ }`);
return {"id": row.id, "type": row.type, "table_row":{"cells": row.table_row.cells.map(cell => (cell.length) ? cell : [new_cell] )}}
});
return table_rows
});
// テーブルの末尾に一括追加 object="block" の prop を追加することが(たぶん)必要
await notion.blocks.children.append({
block_id: table_id,
children: new_table_rows.map(item => {let copied = {...item}; copied.object="block"; return copied })
});
// Promise の直列処理 この辺の正当なやり方がわからない
await new_table_rows.reduce((promise, item) => {
return promise.then(async () => {
await new Promise(s => setTimeout(s, 1000)).then( // もうちょっと早くしても大丈夫かも
await notion.blocks.delete({ "block_id": item.id })
).then(
response => console.log(response)
)
});
}, Promise.resolve()
)
結果:左 → 右
別の方法:callout の子要素にして、位置を保ちつつテーブルごと差し替える
- callout の id からテーブルの id を取得する
- テーブルの id から行データの id を取得する
- texコードを追加した更新後の行データを作成する
- 更新後の行データから、
table row block
オブジェクトのリストを作成する -
table block
オブジェクトを作成し、そのchildren
prop に上で作成したオブジェクトのリストを設定する - この
table block
オブジェクトを、callout の子要素(新規テーブル)として追加する - 元のテーブルを削除する
↑の5の処理が、公式doc の table block の説明にある以下の文章の具体的な実施法になっている[1]
When creating a table block via the Append block children endpoint, the table must have at least 1 table_row whose cells array has the same length as the table_width.
const parent_url = "https://www.notion.so/~~ページのid~~#~~コールアウトのid~~"
const [page_id, parent_id] = parent_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);
let table_id = ""
let headers = [false, false]; // ヘッダー色付けの設定を元のテーブルに合わせる
let header_row_obj = undefined; // ラベル行を元のテーブルから流用する
// 親要素の id から子要素(のリスト)を取得 → 子要素(テーブル)から行データを取得 → texコードの作成と挿入
const new_table_rows = await notion.blocks.children.list({ block_id:parent_id }).then( async response => {
table_id = response.results[0].id;
headers = [response.results[0].table.has_column_header, response.results[0].table.has_row_header];
return await notion.blocks.children.list({ block_id:table_id })
}).then( response => { // ここ以下は前の手法と同じ
const org_table_rows = response.results.map(x => { const {id, type, table_row} = x; return {id, type, table_row} });
header_row_obj = org_table_rows[0]; // ラベル行を保存しておく
const table_rows = org_table_rows.slice(1).map( row => {
const color_code = row.table_row.cells.map( cell => (cell.length) ? cell[0].plain_text : "" ).find(text => text.includes("#"));
const new_cell = set_celldata_obj(true, `\\colorbox{${color_code}}{ }`);
return {"id": row.id, "type": row.type, "table_row":{"cells": row.table_row.cells.map(cell => (cell.length) ? cell : [new_cell] )}}
});
return table_rows
});
// ラベル行のデータと他の行データを結合し、table row block object のリストとして整える
const row_blocks = [header_row_obj].concat(new_table_rows).map(item => {
return { "object": 'block', "type": item.type, "table_row": item.table_row }
})
// 子要素として table row block object のリストを持つ、table block object を作成する
const table_object = { "object": 'block',
"type": "table",
"has_children": true,
"table": { "table_width": header_row_obj.table_row.cells.length, // ラベル行のセル数を流用
"has_column_header": headers[0], // 元のテーブルに合わせる ↓も同じ
"has_row_header": headers[1],
"children": row_blocks // [table row block object, ...] の形式
}
}
// 親要素(ここでは callout)に、子要素として table block を追加する
await notion.blocks.children.append({
block_id: parent_id,
children: [table_object]
}).then(async () => { // 追加が終わったら、元のテーブルは削除する
return await notion.blocks.delete({ "block_id": table_id })
}).then(
response => console.log(response)
)
-
children を与えずに table block を追加しようとすると、「children をちゃんと設定してね」的なエラーメッセージが出る ↩︎
作った