🥵

Notion APIのDatabase objectにpropertiesの中身が含まれなくなったので対応する

2022/08/29に公開約9,400字

どうしてもタイトルが短くなりませんでしたこんにちは。

以前まではNotionのSDKなどでQuery a databaseを叩くとそのデータベース内のPageのPropertiesの内容を全て含めて返していたのですが、2022/6/28のUpdateを受けてパフォーマンスの低下などの理由からこれが含まれなくなりました。

While convenient, returning accurate results for all properties resulted in bad performance and timeouts for larger databases or pages with lots of mentions. To combat performance, on March 1st, we added a disclaimer that page objects stopped returning accurate results for pages with more than 25 mentions to other objects (which affected properties of type title, rich_text, relation, people, rollup, and formula).

その代わりにproperty_idからPropertyの中身を取得するAPIが新規に追加されたようです。

Versionでいうと、

  • Notion API: 2022-06-28~
  • Notion SDK: v2.0.0~
    にUpdateした時点でこの仕様が適応されます。

以前の仕様は本当に便利だったけど、無料でこの仕様はぶっ壊れだったので、妥当だと思う反面、一度便利さを覚えてしまったが故に禁断症状が出てしまうという性に悩まされている方も多いかと存じます。

無論、以前のversionのAPIを使用すればしっかりとPropertiesの中身が一発で取れますが、常に最先端を生きている人にとってこれは辛いことでしょう。

今回はJavaScriptのSDKを使ってNotion APIでNotionのDatabaseをCMS的に運用していた(主にBlog的な運用をしていた)人向けにどのようにコードを置き換えればいいのかを書いていきたいと思います。

過去のResponseとの比較

以前までのResponse

Notion SDKのRelease情報によるとv1.04すなわちv1系までであれば、以前のPropertiesの中身を含んでいることになります。(Notion APIのVersionについてはここを参照。)

Query a databaseを叩くとpropertiesにはそれぞれ中身を含んでいることがわかります。

{
  "object": "list",
  "results": [
    {
      "object": "page",
      "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
      "created_time": "2022-07-29T08:31:00.000Z",
      "last_edited_time": "2022-08-28T05:46:00.000Z",
      "created_by": {
        "object": "user",
        "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "last_edited_by": {
        "object": "user",
        "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "cover": null,
      "icon": null,
      "parent": {
        "type": "database_id",
        "database_id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "archived": false,
      "properties": { // 本題のproperties
        "Category": {
          "id": "%3Ctvw",
          "type": "select",
          "select": {
            "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
            "name": "programming",
            "color": "green"
          }
        },
        "Status": {
          "id": "%40%7Dw%5B",
          "type": "select",
          "select": {
            "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
            "name": "PUBLISH",
            "color": "orange"
          }
        },
        "Date": {
          "id": "Yp%3F%3B",
          "type": "date",
          "date": {
            "start": "2022-07-29",
            "end": null,
            "time_zone": null
          }
        },
        "Tags": {
          "id": "%5C%5EBo",
          "type": "multi_select",
          "multi_select": [
            {
              "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
              "name": "JavaScript",
              "color": "yellow"
            },
            {
              "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
              "name": "DnD",
              "color": "purple"
            },
            {
              "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
              "name": "React",
              "color": "blue"
            }
          ]
        },
        "Title": {
          "id": "title",
          "type": "title",
          "title": [
            {
              "type": "text",
              "text": {
                "content": "ドラッグアンドドロップ完全に理解した",
                "link": null
              },
              "annotations": {
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
              },
              "plain_text": "ドラッグアンドドロップ完全に理解した",
              "href": null
            }
          ]
        }
      },
      "url": "https://www.notion.so/xxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  ],
  "next_cursor": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
  "has_more": true,
  "type": "page",
  "page": {}
}

これにより、CMS的な運用が簡単にできるような仕様でした。(Blogのタイトルやカテゴリなどが取得できているからこれを表示さればいいので)

Notion API 2022-06-28以降のResponse

Query a databaseを叩くとpropertiesはproperty名とidしか取得できないことがわかります。(Titleのidがtitleになっているのは多分DB作成時のデフォルトのProperty名使っている場合はこうなるのかと思います。)

{
  "object": "list",
  "results": [
    {
      "object": "page",
      "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
      "created_time": "2022-07-29T08:31:00.000Z",
      "last_edited_time": "2022-08-28T05:46:00.000Z",
      "created_by": {
        "object": "user",
        "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "last_edited_by": {
        "object": "user",
        "id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "cover": null,
      "icon": null,
      "parent": {
        "type": "database_id",
        "database_id": "xxxx-xxxx-xxxx-xxxx-xxxxxxx"
      },
      "archived": false,
      "properties": { // 本題のproperties
        "Category": {
          "id": "%3Ctvw" // idしかない...
        },
        "Status": {
          "id": "%40%7Dw%5B"
        },
        "Date": {
          "id": "Yp%3F%3B"
        },
        "Tags": {
          "id": "%5C%5EBo"
        },
        "Title": {
          "id": "title"
        }
      },
      "url": "https://www.notion.so/xxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  ],
  "next_cursor": "xxxx-xxxx-xxxx-xxxx-xxxxxxx",
  "has_more": true,
  "type": "page",
  "page": {}
}

おーまいがー

なのでPropertiesの中身を知るにはRetrieve a page property itemというものを新たに叩いて取得するしかなくなりました。

上記の例でNotion SDKを使ってCategoryを取得するとこんな感じです。(公式Doc参考)

const { Client } = require('@notionhq/client');

const notion = new Client({ auth: process.env.NOTION_API_KEY });

(async () => {
  const pageId = 'xxxx-xxxx-xxxx-xxxx-xxxxxxx';
  const propertyId = "%3Ctvw"
  const response = await notion.pages.properties.retrieve({ page_id: pageId, property_id: propertyId });
  console.log(response);
})();

つまり、これをpropertyを一つずつ叩かなくてはならなくなりました。

(必要に応じてAPIを叩いてデータを取得したほうがパフォーマンス的にはいいので当たり前っちゃ当たり前?)

実際の実装方法

取得する際の関数だけ書きますので、各自で応用してください。

Notion SDK Clientのimport(ES6で書いてます)

import { Client } from '@notionhq/client';

export const notion = new Client({
  auth: process.env.NOTION_INTERNAL_INTEGRATION_TOKEN,
});

以後はこのnotionを使用

次にPageのPropertを取得する関数を先に定義し、それを使って、それぞれのPropertyを取得する関数を一つ一つ定義しています。

import { notion } from './client';

/* PageのPropertを取得 */
export const getProperty = async (pageId: string, propertyId: string) => {
  const response = await notion.pages.properties.retrieve({
    page_id: pageId,
    property_id: propertyId,
  });

  return response;
};

/* PageのTitleを取得 */
export const getTitleInProperty = async (
  pageId: string,
  propertyId: string
) => {
  const response = await getProperty(pageId, propertyId);

  if (!response) return null;
  if (!('results' in response)) return null;
  if (!('title' in response.results[0])) return null;

  return response.results[0].title.plain_text;
};
/* PageのCategoryを取得 */
export const getCategoryInProperty = async (
  pageId: string,
  propertyId: string
) => {
  const response = await getProperty(pageId, propertyId);

  if (!response) return null;
  if (!('select' in response)) return null;
  if (!response.select) return null;

  return response.select.name;
};
/* PageのDateを取得 */
export const getDateInProperty = async (pageId: string, propertyId: string) => {
  const response = await getProperty(pageId, propertyId);

  if (!response) return null;
  if (!('date' in response)) return null;
  if (!response.date) return null;

  return response.date.start;
};

TypeScriptでNotionのSDKを使うとそれぞれの型ガードを書かなければならなくなるのと、全てのpropertiesが必要でもない限りはこのように分けて書いた方が良いと思いました。

また、Propertyを取得する関数は必要な値のみを返すようにしていますが、これはお好みで。

次にBlog一覧を取得してそのproperty_idからPropertyの中身を取得する関数を定義します。(これ多分もうちょっと型とかキレイに書ける…)

/* ブログの記事一覧を取得 */
export const getBlogPosts = async (): Promise<BlogPost> => {
  const databaseId = 'xxxx-xxxx-xxxx-xxxx-xxxxxxx';
  const posts = await notion.databases.query({
    database_id: databaseId,
  });

  const responseWithProperties = await Promise.all(
    posts.results.map(async (post) => {
      if (!('properties' in post)) return null;
      const pageId = post.id;
      const titlePropertyId = post.properties.Title.id;
      const categoryPropertyId = post.properties.Category.id;
      const datePropertyId = post.properties.Date.id;

      /* 必要となるPropertiesの取得 */
      const title = await getTitleInProperty(pageId, titlePropertyId);
      const category = await getCategoryInProperty(pageId, categoryPropertyId);
      const atDate = await getDateInProperty(pageId, datePropertyId);

      const postInfo = {
        id: pageId,
        title: title || '',
        category: category || 'なし',
        atDate: atDate || '',
      };

      return postInfo;
    })
  );

  return responseWithProperties.filter((post) => post !== null) as BlogPost;
};

最終的に返すpostInfoの形式はお好みで。(私はブログ一覧ページに必要な情報を最低限にしたBlogPostというobjectを自分で定義して返すようにしてます。)

type BlogPost = {
  id: string;
  title: string;
  category: string;
  atDate: string;
}[];

これで今まで通りにNotionのDBをCMSっぽく運用することができます。

ただ一気にコード量が膨らみましたね。デモタノシイカラダイジョウブデス。

Discussion

ログインするとコメントできます