not/onNOT DONE, ALWAYS ON

not/onの開発メモ:NotionClient編

ver.2.7

not/onの公開記事をより、止まらせず(NOT DONE)、自走する(ALWAYS ON)ものにするため、ダイナミックな記事生成のパーツを検討する。その第2弾。

第1弾はこちら | not/onの開発メモ:FunkApiCore編not/on の公開記事をより、 止まらせず(NOT DO NE)、動く(ALWAYS ON) ものにするため、ダイナミックな記事生成のパーツを検討する。 funkApiCore は、これを実現するにあたり、 not/on エンジンが呼び出すライブラリとして作成する。 not/o…not/on

NotionデータベースのデータをAWS Lambda(Python)でプレーンなJSONに変換し、前段の Amazon CloudFront でキャッシュすることで、Notion APIのレートリミットを回避し、高速かつ安全にフロントエンドから取得可能にする汎用アセット。

利用者が事前に「管理用API」へNotionの接続情報を登録し、安全な「アクセスキー」を発行。

not/onの記事が、Notionデータベースと連動して自走する、
これを実現するためのAPIと呼び出しライブラリを実装。

尚、not/onのための実装だが、アクセスキー認証とCORSフリーの設計により、誰でも、どのAPPでも安全に使えます。




NotionClient 開発ドキュメント

🚀 コンセプト

安全なマルチテナント対応

事前に管理用API経由でNotionのインテグレーションシークレットとデータベースIDを登録し、代わりにフロントエンド公開用の「アクセスキー」を発行。生のAPIキーがブラウザ側に漏洩するリスクを完全に排除。

高パフォーマンス・低コスト

ユーザーのブラウザは CloudFront エッジキャッシュにアクセスするため、Lambda の起動回数および Notion API の呼び出し回数を劇的に削減。CloudFront側でアクセスキーをキャッシュキーに含める設定を行うことで、複数ユーザーによるマルチテナント環境でも個別のキャッシュ配信とNotionのレートリミット(平均3リクエスト/秒)回避を両立。
Notionには迷惑をかけない。

完全な汎用性

Notion側で列(プロパティ)の追加・削除を行っても、APIやJSのコード修正は不要。

初期化の簡素化

NotionClient.js 内部に CloudFront のエンドポイントを隠蔽。利用側は発行されたアクセスキーを渡すだけでインスタンス化可能。
描画ロジックの分離: クライアントライブラリはデータ取得(Fetch)に特化。


アーキテクチャ


1. 初期設定フロー(開発者・管理者) サービス管理者 Notion管理画面 / ツール client.register() Notionキー + DB IDの登録 POST /register 汎用Notionプロキシ (CloudFront + Lambda) バックエンドで認証情報を隠蔽 アクセスキー発行 notion_ack_xxxx Notion DB ユーザーが所有する 任意のデータベース 2. データ配信・利用フロー(一般閲覧者 / Webサイト) 一般公開サイト not/on記事 / 各種App NotionClient.js └ accessKey埋め込み データ要求 (CORSフリー) GET /query?access_key=... エッジキャッシュ層 (キーごとに高速返却) キャッシュ未検出時のみ 裏側でNotionへ安全にクエリ 平坦化済みクリーンJSON [{ id, タイトル, タグ }, ...] Notion DB 負荷をかけずに データを安全に維持


1. 接続情報の登録(とアクセスキーの発行)

[利用者] ──(Notionキー + DB ID)──> [管理用API] ──(暗号化)──> [Amazon DynamoDB] │ (アクセスキー発行)

2. クライアントからのデータ閲覧(通常時)

[閲覧者ブラウザ] ──(アクセスキー)──> [Amazon CloudFront] ──(キー毎キャッシュ)──> [データ取得API (Lambda)] ──> [Notion DB]



使い方(フロントエンド)

NotionClient.js をお使いのフロントエンドに配置してインポート。


📖 API 応答フォーマット(JSON)

CloudFront エッジから、以下のように型が平坦化(String, Number, Boolean, Array)されたオブジェクトの配列が高速返却される。

[
  {
    "id": "cbc3013d-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
    "タイトル": "Notion記事サンプル",
    "タグ": ["技術", "AWS"],
    "公開日": "2026-07-05",
    "ステータス": "公開中",
    "閲覧数": 1250,
    "有効化": true
  }
]

💻 クライアントライブラリ(JS)詳細リファレンス

クラス: NotionClient

async register(notionApiKey, databaseId)

Notionの接続情報を管理用APIへ登録し、新しいアクセスキーを発行してインスタンス内に自動セットする。

constructor(accessKey)

内部リソースとして固定された CloudFront エンドポイントの初期化、および固有のアクセスキーの設定。

引数: accessKey {string}(必須): 管理画面から発行された固有のアクセスキー。

async getRows(options = {})

アクセスキーをクエリパラメータに付与し、CloudFront から整形済みのレコード配列を非同期で取得(キー毎のキャッシュ有効)。

戻り値: Promise<Array>: 平坦化されたオブジェクトの配列。

💡 使い方・実装例

1. NotionのAPIキーとデータベースIDの取得

Notionから「インテグレーションシークレット」と「データベースID」を取得する手順。

インテグレーションシークレット (APIキー) の取得

データベースID の取得と接続許可

通常のURLの場合:
https://www.notion.so/ワークスペース名/【この32桁の英数字】?v=...
ドメインが短い場合:
https://www.notion.so/【この32桁の英数字】?v=...

⚠️ 注意: ?v= より前のスラッシュ(/)に挟まれた32桁の文字列(ハイフンは除外するか、そのまま使用)がデータベースID。

これらが揃えば、アクセスキーを発行できる。

2. アクセスキーの発行

<!DOCTYPE html>
<html lang="ja">
<head>
  &lt;meta charset="UTF-8">
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0">
  &lt;title> NotionClient - アクセスキー発行管理ツール&lt;/title>
  &lt;style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      margin: 2rem auto;
      max-width: 600px;
      padding: 0 1rem;
      background-color: #fafafa;
      color: #1f2937;
    }
    h1 {
      font-size: 1.5rem;
      border-bottom: 2px solid #e5e7eb;
      padding-bottom: 0.5rem;
    }
    .warning {
      background-color: #fffbeb;
      border: 1px solid #fef3c7;
      color: #b45309;
      padding: 1rem;
      border-radius: 0.375srem;
      font-size: 0.875rem;
      margin-bottom: 1.5rem;
    }
    .form-group {
      margin-bottom: 1.25rem;
    }
    label {
      display: block;
      font-weight: bold;
      margin-bottom: 0.375rem;
      font-size: 0.875rem;
    }
    input[type="text"], input[type="password"] {
      width: 100%;
      padding: 0.625rem;
      border: 1px solid #d1d5db;
      border-radius: 0.375rem;
      box-sizing: border-box;
      font-family: monospace;
    }
    button {
      background-color: #2563eb;
      color: white;
      padding: 0.625rem 1.25rem;
      border: none;
      border-radius: 0.375rem;
      font-weight: bold;
      cursor: pointer;
      width: 100%;
    }
    button:hover {
      background-color: #1d4ed8;
    }
    button:disabled {
      background-color: #9ca3af;
      cursor: not-allowed;
    }
    .result-box {
      margin-top: 1.5rem;
      padding: 1rem;
      background-color: #ecfdf5;
      border: 1px solid #d1fae5;
      border-radius: 0.375rem;
      display: none;
    }
    .result-box.error {
      background-color: #fef2f2;
      border: 1px solid #fee2e2;
      color: #991b1b;
    }
    .key-display {
      background-color: #ffffff;
      padding: 0.75rem;
      border: 1px solid #a7f3d0;
      border-radius: 0.25rem;
      font-family: monospace;
      font-size: 1.125rem;
      color: #065f46;
      word-break: break-all;
      margin-top: 0.5rem;
    }
  &lt;/style>
</head>
<body>

  <h1>Notion 接続設定・アクセスキー発行</h1>
  
  <div class="warning">
    <strong>⚠️ セキュリティ注意</strong><br>
    この画面は生シークレット(Notion APIキー)を取り扱います。一般公開サイトには配置せず、ローカル環境での実行か、関係者のみがアクセスできる安全な環境(認証制限下)でのみ利用してください。
  </div>

  <div class="form-group">
    <label for="notionApiKey">Notion インテグレーションシークレット (APIキー)</label>
    <input type="password" id="notionApiKey" placeholder="secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
  </div>

  <div class="form-group">
    <label for="databaseId">Notion データベースID</label>
    <input type="text" id="databaseId" placeholder="32桁の英数字(ハイフンなし)">
  </div>

  <button id="submitBtn">アクセスキーを発行</button>

  <div id="resultBox" class="result-box">
    <div id="resultMessage"></div>
    <div id="keyDisplay" class="key-display"></div>
  </div>

  &lt;script type="module">
    import { NotionClient } from './NotionClient.js';

    const notionApiKeyInput = document.getElementById('notionApiKey');
    const databaseIdInput = document.getElementById('databaseId');
    const submitBtn = document.getElementById('submitBtn');
    const resultBox = document.getElementById('resultBox');
    const resultMessage = document.getElementById('resultMessage');
    const keyDisplay = document.getElementById('keyDisplay');

    submitBtn.addEventListener('click', async () => {
      const notionApiKey = notionApiKeyInput.value.trim();
      const databaseId = databaseIdInput.value.trim();

      if (!notionApiKey || !databaseId) {
        alert('両方の項目を入力してください。');
        return;
      }

      // UI状態の初期化
      submitBtn.disabled = true;
      submitBtn.innerText = '発行中...';
      resultBox.style.display = 'none';
      resultBox.classList.remove('error');
      keyDisplay.style.display = 'block';

      try {
        // 初期化時はアクセスキー指定なしの adminClient を生成
        const adminClient = new NotionClient();
        
        // ライブラリの登録関数をコール
        const result = await adminClient.register(notionApiKey, databaseId);

        // 成功時の描画
        resultMessage.innerHTML = '<strong>登録に成功しました。</strong><br>以下のアクセスキーをコピーし、フロントエンド(公開Webサイト側)の初期化コードに埋め込んでください。';
        keyDisplay.innerText = result.access_key;
        resultBox.style.display = 'block';

        // 入力フォームをクリア(セキュリティ保護のため)
        notionApiKeyInput.value = '';
        databaseIdInput.value = '';

      } catch (error) {
        // エラー時の描画
        resultBox.classList.add('error');
        resultMessage.innerHTML = '<strong>エラーが発生しました。</strong>';
        keyDisplay.innerText = error.message || error;
        resultBox.style.display = 'block';
      } finally {
        submitBtn.disabled = false;
        submitBtn.innerText = 'アクセスキーを発行';
      }
    });
  &lt;/script>
</body>
</html>

3. データ取得

ご利用のフロントエンドAPPにNotionClientライブラリをインポート、
下記は、データ取得の実装例。

import { NotionClient } from './NotionClient.js'

&lt;script type="module">
  import { NotionClient } from './NotionClient.js';

  // 事前に発行した「安全なアクセスキー」のみを埋め込む
  const client = new NotionClient('notion_ack_abc123_取得したキー');

  async function initTable() {
    try {
      const articles = await client.getRows();
      const tbody = document.querySelector('#article-table tbody');
      
      tbody.innerHTML = articles.map(item => `
        <tr>
          <td>${item.タイトル || '無題'}</td>
          <td>${Array.isArray(item.タグ) ? item.タグ.join(', ') : (item.タグ || '-')}</td>
        </tr>
      `).join('');
    } catch (error) {
      console.error('データ取得失敗', error);
    }
  }

  document.addEventListener('DOMContentLoaded', initTable);
&lt;/script>

setupAndFetch();

ライブラリ情報

OpenAPI 3.0.0 仕様書(JSON)

{
  "openapi": "3.0.3",
  "info": {
    "title": "Generic Notion Proxy & Multi-Tenant API",
    "description": "Notionの接続情報を安全に管理し、発行されたアクセスキーを用いて、平坦化(フラット化)されたプレーンなJSONデータを配信するマルチテナント対応プロキシAPI。",
    "version": "1.1.0"
  },
  "servers": [
    {
      "url": "https://YOUR_CLOUDFRONT_DISTRIBUTION_URL.cloudfront.net",
      "description": "Amazon CloudFront ディストリビューションエンドポイント"
    }
  ],
  "paths": {
    "/register": {
      "post": {
        "summary": "Notion接続情報の登録とアクセスキーの発行",
        "description": "利用者のNotion APIキーとデータベースIDをデータベースに安全に登録し、フロントエンド公開用の固有のアクセスキーを発行します。",
        "operationId": "registerTenant",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "登録成功。アクセスキーが発行されます。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResponse"
                }
              }
            }
          },
          "400": {
            "description": "リクエスト不正。必須パラメータの不足など。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "サーバー内部エラー。データストアへの書き込み失敗など。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/query": {
      "get": {
        "summary": "整形済みNotionデータの取得(キャッシュ有効)",
        "description": "クエリパラメータで指定されたアクセスキーに紐づくNotionデータベースからレコードをクエリし、データ型を平坦化したオブジェクトの配列を返します。前段のCloudFrontでキー毎にキャッシュされます。",
        "operationId": "getNotionRows",
        "parameters": [
          {
            "name": "access_key",
            "in": "query",
            "description": "管理画面または/registerAPIから発行された固有のアクセスキー",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "notion_ack_cbc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
          }
        ],
        "responses": {
          "200": {
            "description": "正常処理。フラットなオブジェクトの配列が返ります。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NotionRow"
                  }
                },
                "example": [
                  {
                    "id": "cbc3013d-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
                    "タイトル": "Notion記事サンプル",
                    "タグ": ["技術", "AWS"],
                    "公開日": "2026-07-05",
                    "ステータス": "公開中",
                    "閲覧数": 1250,
                    "有効化": true
                  }
                ]
              }
            }
          },
          "401": {
            "description": "認証エラー。無効なアクセスキーが指定された場合。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "サーバー内部エラー。Notion APIとの通信失敗やパースエラー。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "RegisterRequest": {
        "type": "object",
        "required": ["notion_api_key", "database_id"],
        "properties": {
          "notion_api_key": {
            "type": "string",
            "description": "Notionのインテグレーションシークレットトークン",
            "example": "secret_xX0yY1zZ..."
          },
          "database_id": {
            "type": "string",
            "description": "対象とするNotionデータベースのID(32桁の英数字)",
            "example": "6bc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
          }
        }
      },
      "RegisterResponse": {
        "type": "object",
        "properties": {
          "access_key": {
            "type": "string",
            "description": "フロントエンド公開用に発行された固有のアクセスキー。データ取得APIの認証に利用。",
            "example": "notion_ack_6bc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
          }
        }
      },
      "NotionRow": {
        "type": "object",
        "description": "Notionの1行(ページ)データをフラットに変換したオブジェクト。キー名はNotionの列名(プロパティ名)がそのまま動的にマッピングされる。",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "NotionページのユニークID"
          }
        },
        "additionalProperties": {
          "oneOf": [
            { "type": "string", "description": "テキスト、セレクト、日付、URLなど" },
            { "type": "number", "description": "数値プロパティ" },
            { "type": "boolean", "description": "チェックボックス" },
            { 
              "type": "array", 
              "items": { "type": "string" },
              "description": "マルチセレクトなど複数値を持つプロパティ"
            },
            { "type": "null" }
          ]
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "エラーメッセージの詳細"
          }
        }
      }
    }
  }
}
not/on