このブログははてなブログからの移行記事です。
書いた
notion-sdk-jsとは
Notion APIを叩くためのTypeScriptのライブラリ。多分公式。
何を解決したいのか
Pageを作成するAPIを叩く際にJSONを組み立てるのがめんどくさいのが一番だった。
型定義で守られてはいるものの、ドキュメントを見ないとまず書けないし後は書く分量がどうしても多い。
例えばHeading 1, Paragraphの2つだけで構成した下記のページを作る
この場合、こんなコードを書く必要がある。
import { Client } from '@notionhq/client'; const client = new Client({ auth: 'YOUR_NOTION_API_TOKEN', }); await client.pages.create({ parent: { databse_id: 'DATABASE_ID', }, properties: {}, children: [ { type: 'heading_1', heading_1: { rich_text: [ { type: 'text', text: { content: 'Section1', }, }, ], }, }, { type: 'paragraph', paragraph: { rich_text: [ { type: 'text', text: { content: 'I am ', }, }, { type: 'text', text: { content: 'engineer', }, annotations: { bold: true, }, }, ], }, }, ], });
長くないですか?僕は長いと思って毎回書いてられん…と思ってしまいました。
また、地味に仕様上めんどくさいなと思ったポイントとしては文字列を渡す際、ほとんどはRich textを配列形式で渡す必要があることです。
上記のコードから抜粋すると以下です
await client.pages.create({ // ... children: [ // ... { type: 'paragraph', paragraph: { rich_text: [ { type: 'text', text: { content: 'I am ', }, }, { type: 'text', text: { content: 'engineer', }, annotations: { bold: true, }, }, ], }, }, ], });
ここで配列で渡す理由は、文章としては1つでも文字1つ1つにDecorateion(Notion APIの言葉で言うとAnnotations)が振られる可能性があるため、それを仕様として吸収するために配列形式になってます。
Notionの仕様上、しょうがないのは納得できたんですがAPIを利用時に毎回そこそこ大きめなObjectを羅列するのは辛いなと思いました。
ちょっとでも楽しようと思って作った
と言うわけで作った。
先ほどのコード例はこれを使うとこうなる。
import { BlockObjects, RichTextObjects, CustomTypes } from '@sota1235/notion-sdk-js-helper'; import { Client } from '@notionhq/client'; const { heading1, paragraph, } = BlockObjects; const client = new Client({ auth: 'YOUR_NOTION_API_TOKEN', }); // Use helper methods when create page await client.pages.create({ parent: { databse_id: 'DATABASE_ID', }, properties: {}, children: [ heading1('Section 1'), paragraph([ RichTextObjects.richText('I am '), RichTextObjects.richText('engineer', { bold: true, }), ]), ], });
さっきよりは書きやすい & 読みやすくなりました。
この量だと書けばいいじゃん、となるかもしれませんが実際に作りたいページはもう少し複雑なのでこれがあるだけでだいぶ書き味が変わるはずです。
下記ページに大体のBlockのサンプルコードを載せてるのでそちらを読むとよりイメージが湧くと思います。
Example page for notion-sdk-js-helper
地味に面倒だと思ってた文字列の配列に関しても、少しでも楽に書けるようfunctionの入口はstring | RichText | RichText[]
(RichTextの型)を受け付けつつ、各部分でその部分を吸収するようにしました。
await client.pages.create({ parent: { databse_id: 'DATABASE_ID', }, properties: {}, children: [ paragraph('text'), // stringだけ渡すのでもOK paragraph(RichTextObjects.richText('engineer', { bold: true, }), ), // RichTextだけ渡すのでもOK paragraph([ RichTextObjects.richText('I am '), RichTextObjects.richText('engineer', { bold: true, }), ]), // RichTextの配列もOK(これが元々の仕様) ], });
これでだいぶストレスフリーに書けるはずです。
※ サンプルコードを書いてて気づいたけどstring
とRichText
が混ぜこぜの配列も渡せるようにした方が楽そうなので改善しておきます
作るにあたり
ここからは苦労談義なので興味がなければ読み飛ばしてください
型定義をどうするか
ブロック1つ1つのObjectを生成するfunctionsを作るにあたり、それぞれのブロックの型定義が欲しくなります。
notion-sdk-jsは結構、堅牢な型定義があるのでそれを利用すればすんなりいけるだろうと思ったら「これが欲しい…!」と言う型はexportされていませんでした。
exportするだけだしPull Request出そうかなと思ったんですが質問はなぜかメールでしか受け付けておらず、質問してみたら「いいアイディアだね!いつかは検討するけど他にもいろんなfeature requestがあるから待っててね!(意訳)」と言う感じの回答が返ってきたので本家のコードを直すのは諦めた。
代わりに欲しい型を取り出すAdHocな方法を教えてもらったのでいまいちとは思いつつそれを参考に型定義を作った。
具体的には以下のファイルに色々書いてて、読むと分かるんですがあまり綺麗な方法ではないです。
Undocumentedな仕様がたまに紛れてる
直感的にはいけそうなんだけど、実際にAPIを叩くとうまくいかない部分がちょいちょいありました。
つまるところ、notion-sdk-jsの型通りに使ってれば壊れることはないので利用者は特に意識する必要はないのですが例えばColumn Listの子BlockにTableは突っ込めないという制約があったりします(手動で書くと当然作れる)。
まとめ
- 楽したいので自分のために作ったけど似たような需要があれば使ってください
- メンテナンス頑張りますけどバグってたらIssueかPRください
- 普段お世話になってるyhatt/jsx-slackから発想を得たりしました。JSXで書けるようにしたらおもろそうだし勉強になりそうだなと思ったけど流石に高級だなと思って愚直に書くだけの実装にとどめました