# 機能設計書 77-イベントフック

## 概要

本ドキュメントは、preSave、postSave、prePublish、postPublish、preUnpublish、postUnpublishの6種類のイベントフックを登録・実行する機能の設計を記述する。

### 本機能の処理概要

本機能は、エントリの保存・公開・非公開操作の前後にカスタム処理を挿入できるイベントフックシステムを提供する。グローバルレジストリにハンドラを登録し、対応するライフサイクルイベント発生時に登録された全ハンドラを順次実行する。

**業務上の目的・背景**：CMSの保存・公開フローにおいて、データの自動変換、外部API連携、バリデーション強化等のカスタム処理を挿入したい要件がある。イベントフックにより、CMSコアのコードを変更せずにこれらのカスタム処理を追加できる。

**機能の利用シーン**：
- preSave: エントリ保存前にデータの自動変換（日付フィールドの自動設定、スラッグの自動生成等）
- postSave: 保存後に外部サービスへの通知（Slack通知、Webhook呼び出し等）
- prePublish: 公開前の追加バリデーション
- postPublish: 公開後のキャッシュ無効化、CDNパージ等
- preUnpublish: 非公開前の確認処理
- postUnpublish: 非公開後のクリーンアップ処理

**主要な処理内容**：
1. **イベントリスナー登録（registerEventListener）**: イベント名とハンドラ関数のペアをレジストリに登録。オプショナルなoptionsパラメータもサポート
2. **イベントリスナー取得（getEventListeners）**: 指定イベント名に登録されたハンドラ一覧をスプレッドコピーで返す
3. **イベントリスナー削除（removeEventListener）**: 特定のハンドラまたはイベント名に対する全ハンドラを削除
4. **イベント実行（invokeEvent）**: 登録されたハンドラを順次非同期実行。ハンドラがundefined以外の値を返した場合、エントリデータを更新して次のハンドラに渡すチェーンパターンをサポート

**関連システム・外部連携**：レジストリ（registry.js）と連携。entries.ts（エントリ保存処理）、editorialWorkflow.ts（公開・非公開処理）から呼び出される。

**権限による制御**：イベントフック登録自体には権限制御はない。CMS.registerEventListener() APIとして公開される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主画面 | preSave・postSaveイベントフックの実行。エントリ保存前後にカスタム処理を実行 |
| 7 | エントリ新規作成画面 | 参照画面 | 新規エントリ保存時のpreSave・postSaveイベントフック実行 |
| 8 | ワークフロー画面 | 参照画面 | エントリ公開時のprePublish・postPublishイベントフック実行 |

## 機能種別

イベント駆動 / フック管理

## 入力仕様

### 入力パラメータ

**registerEventListener**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | イベント名（'preSave'/'postSave'/'prePublish'/'postPublish'/'preUnpublish'/'postUnpublish'） | allowedEventsに含まれるか検証 |
| handler | function | Yes | イベントハンドラ関数。(data, options) => result を受け取る | - |
| options | object | No | ハンドラオプション（デフォルト: {}） | - |

**removeEventListener**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | イベント名 | allowedEventsに含まれるか検証 |
| handler | function | No | 削除対象のハンドラ。未指定時は全ハンドラ削除 | - |

**invokeEvent**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | イベント名 | allowedEventsに含まれるか検証 |
| data | object | Yes | イベントデータ（entry等を含む） | - |

### 入力データソース

- CMS.registerEventListener() API経由でハンドラが登録される
- entries.ts / editorialWorkflow.ts のアクションから invokeEvent が呼ばれる

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| eventListeners | array | { handler, options } の配列 |
| updatedData | any | invokeEventの戻り値（ハンドラにより変更されたエントリデータ） |

### 出力先

- getEventListeners: ハンドラ配列のコピーを返す
- invokeEvent: 変更済みの entry.get('data') を返す

## 処理フロー

### 処理シーケンス

```
1. イベントリスナー登録
   └─ registerEventListener({ name, handler }, options)
   └─ validateEventNameでイベント名検証
   └─ registry.eventHandlers[name]配列に{ handler, options }を追加
2. イベント発火（invokeEvent）
   └─ validateEventNameでイベント名検証
   └─ 登録ハンドラを取得
   └─ for...of ループで各ハンドラを順次await実行
   └─ ハンドラの戻り値がundefinedでない場合、data.entry.set('data', result)で更新
   └─ 最終的なentry.get('data')を返す
3. イベントリスナー削除
   └─ removeEventListener({ name, handler })
   └─ handler指定時: filterで特定ハンドラを除外
   └─ handler未指定時: 空配列で全削除
```

### フローチャート

```mermaid
flowchart TD
    A[registerEventListener] --> B[validateEventName]
    B --> C[eventHandlers配列に追加]

    D[エントリ保存/公開操作] --> E[invokeEvent]
    E --> F[validateEventName]
    F --> G[handlers取得]
    G --> H{ハンドラあり?}
    H -->|Yes| I[for...of: handler実行]
    I --> J{result !== undefined?}
    J -->|Yes| K[entry.set data, result で更新]
    J -->|No| L[データ変更なし]
    K --> M{次のハンドラ?}
    L --> M
    M -->|Yes| I
    M -->|No| N[entry.get data を返す]
    H -->|No| N
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-77-01 | 許可イベント名 | allowedEvents: 'prePublish', 'postPublish', 'preUnpublish', 'postUnpublish', 'preSave', 'postSave' の6種類のみ | register/get/remove/invoke時 |
| BR-77-02 | イベント名検証 | allowedEventsに含まれないイベント名はErrorをスロー | 全操作時 |
| BR-77-03 | チェーン実行 | ハンドラは登録順に順次実行される。前のハンドラの結果が次のハンドラのdataに反映される | invokeEvent時 |
| BR-77-04 | データ更新条件 | ハンドラの戻り値がundefinedでない場合のみ、エントリデータが更新される | invokeEvent時 |
| BR-77-05 | コピー返却 | getEventListenersはスプレッド演算子でコピーを返し、元の配列の変更を防ぐ | getEventListeners時 |
| BR-77-06 | 選択的削除 | removeEventListenerはhandler指定時に特定ハンドラのみ削除、未指定時は全削除 | removeEventListener時 |

### 計算ロジック

特になし。

## データベース操作仕様

### 操作別データベース影響一覧

本機能はイベント管理層であり、直接的なデータベース操作は行わない。ハンドラ内で外部APIを呼ぶことは可能だが、それは利用側の責任である。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Error (throw) | allowedEventsに含まれないイベント名 | Invalid event name '${name}' |
| - | ハンドラ例外 | ハンドラ内でエラーが発生した場合 | awaitされるため、呼び出し元のtry-catchで捕捉される |

### リトライ仕様

リトライは利用側（entries.ts等）の責任。invokeEvent自体にはリトライ機能はない。

## トランザクション仕様

イベントハンドラの実行は非同期で順次実行される。途中のハンドラでエラーが発生した場合、残りのハンドラは実行されない（for...ofのawaitにより中断）。

## パフォーマンス要件

- イベントハンドラは非同期関数をサポートするため、ハンドラの実行時間がCMSの応答時間に直接影響する
- preSaveハンドラが重い処理を含む場合、保存操作のユーザー体験が劣化する可能性がある

## セキュリティ考慮事項

- preSaveハンドラはエントリデータを任意に変更できるため、意図しないデータ改竄のリスクがある
- ハンドラ内で外部APIを呼ぶ場合、認証情報の漏洩に注意が必要
- サードパーティプラグインが登録するハンドラのコードは信頼性を確認すべき

## 備考

- invokeEventはasync関数であり、ハンドラもasync関数をサポートする
- ハンドラの戻り値がundefinedの場合はデータ変更なしとみなされる（void関数は変更を行わない）
- _dataオブジェクトはスプレッド演算子でコピーされ、ハンドラ間でのデータ独立性が確保される
- Immutable.jsのentryオブジェクトのset/getメソッドが使用されるため、ハンドラの戻り値はプレーンオブジェクト

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | allowedEventsとeventHandlersの初期化構造 |

**読解のコツ**: eventHandlersは{ [eventName]: Array<{ handler, options }> }の構造。allowedEventsの各名前をキーとする空配列で初期化される。

**主要処理フロー**:
- **7-14行目**: allowedEvents定義（6種類のイベント名）
- **15-18行目**: eventHandlersの初期化。allowedEvents.forEachで各イベント名に空配列を割り当て
- **33行目**: registry.eventHandlersにeventHandlersを格納

#### Step 2: イベント登録・取得・削除を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerEventListener, getEventListeners, removeEventListenerのロジック |

**主要処理フロー**:
- **232-236行目**: validateEventName - allowedEvents.includes(name)チェック。不正名でErrorスロー
- **238-241行目**: getEventListeners - スプレッド演算子[...registry.eventHandlers[name]]でコピー返却
- **243-246行目**: registerEventListener - { handler, options }をpush
- **263-272行目**: removeEventListener - handler指定時はfilterで除外、未指定時は空配列

#### Step 3: invokeEventのチェーン実行を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | invokeEventの非同期チェーン実行ロジック |

**主要処理フロー**:
- **248行目**: async function invokeEvent({ name, data })
- **249行目**: validateEventName(name)
- **250行目**: const handlers = registry.eventHandlers[name]
- **252行目**: let _data = { ...data } - 入力データのコピー
- **253-259行目**: for...of ループ
  - **254行目**: const result = await handler(_data, options) - ハンドラ非同期実行
  - **255-258行目**: result !== undefined の場合
    - **256行目**: const entry = _data.entry.set('data', result) - Immutable.jsのsetでデータ更新
    - **257行目**: _data = { ...data, entry } - 更新されたentryで_dataを再構成
- **260行目**: return _data.entry.get('data') - 最終的なデータを返す

### プログラム呼び出し階層図

```
CMS.registerEventListener({ name, handler }, options)
    |
    +-- validateEventName(name)
    +-- registry.eventHandlers[name].push({ handler, options })

entries.ts / editorialWorkflow.ts
    |
    +-- invokeEvent({ name: 'preSave', data: { entry, ... } })
            |
            +-- validateEventName(name)
            +-- for (const { handler, options } of handlers)
            |       +-- await handler(_data, options)
            |       +-- result !== undefined → entry.set('data', result)
            |
            +-- return _data.entry.get('data')
```

### データフロー図

```
[入力]                              [処理]                          [出力]

registerEventListener       -->  eventHandlers[name].push     -->  ハンドラ配列に追加
  ({ name, handler },            ({ handler, options })
   options)

invokeEvent                 -->  ハンドラ順次実行              -->  変更済みデータ
  ({ name, data })               for...of await                    entry.get('data')
      |                              |
      +-- data.entry          -->  handler(data, options)      -->  result
                                     |
                              result !== undefined ?
                                     |
                              entry.set('data', result)        -->  更新された _data
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| registry.js | `packages/decap-cms-core/src/lib/registry.js` | ソース | イベントフック登録/取得/削除/実行 |
| entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | ソース | preSave/postSaveのinvokeEvent呼び出し元 |
| editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | ソース | prePublish/postPublish/preUnpublish/postUnpublishのinvokeEvent呼び出し元 |
