# 機能設計書：37-ネストドキュメント

## 1. 機能概要

### 1.1 機能の目的
ネストドキュメントプラグイン（@payloadcms/plugin-nested-docs）は、Payload CMSにドキュメントの親子関係（階層構造）とパンくずリスト（breadcrumbs）の自動生成機能を追加する。ページやカテゴリなどの階層的なコンテンツ構造を管理し、ナビゲーションやSEOに活用可能なURL構造を自動生成する。

### 1.2 機能の範囲
- 親ドキュメント参照フィールド（parent）の追加
- パンくずリスト配列フィールド（breadcrumbs）の自動生成
- 親ドキュメント変更時の子ドキュメント自動更新
- ドキュメント作成後の自己参照パンくず更新
- 循環参照防止のフィルタリング
- カスタムURL・ラベル生成関数のサポート

### 1.3 関連する業務フロー
- サイト構造の階層的管理
- パンくずナビゲーションの生成
- SEOフレンドリーなURL構築
- カテゴリ・サブカテゴリ管理

## 2. 機能詳細

### 2.1 プラグイン初期化

#### 処理フロー
```
nestedDocsPlugin(config)
    ↓
対象コレクションをフィルタ
    ↓
parentフィールド追加（未存在時）
    ↓
breadcrumbsフィールド追加（未存在時）
    ↓
フック登録（beforeChange, afterChange）
    ↓
拡張されたConfig返却
```

#### 設定オプション
```typescript
type NestedDocsPluginConfig = {
  collections: CollectionSlug[]      // 対象コレクションスラッグ配列
  breadcrumbsFieldSlug?: string      // breadcrumbsフィールド名（デフォルト: 'breadcrumbs'）
  parentFieldSlug?: string           // parentフィールド名（デフォルト: 'parent'）
  generateLabel?: GenerateLabel      // カスタムラベル生成関数
  generateURL?: GenerateURL          // カスタムURL生成関数
}
```

### 2.2 追加されるフィールド

#### parentフィールド
```typescript
{
  name: 'parent',
  type: 'relationship',
  relationTo: '<collection-slug>',
  maxDepth: 1,
  admin: {
    position: 'sidebar',
  },
  filterOptions: parentFilterOptions()  // 循環参照防止
}
```

#### breadcrumbsフィールド
```typescript
{
  name: 'breadcrumbs',
  type: 'array',
  localized: true,
  admin: {
    readOnly: true,
  },
  fields: [
    { name: 'doc', type: 'relationship', relationTo, maxDepth: 0 },
    { name: 'url', type: 'text' },
    { name: 'label', type: 'text' }
  ]
}
```

### 2.3 フック処理

#### beforeChange: populateBreadcrumbsBeforeChange
保存前にパンくずリストを自動生成

```
ドキュメント保存開始
    ↓
getParents() - 親ドキュメントを再帰取得
    ↓
formatBreadcrumb() - 各ドキュメントをパンくず形式に変換
    ↓
data.breadcrumbs に設定
```

#### afterChange: resaveSelfAfterCreate
新規作成後に自身のIDをパンくずリストに追加

```
create操作時のみ実行
    ↓
パンくずリスト最終要素のdocを自身のIDに更新
    ↓
payload.update() で再保存
```

#### afterChange: resaveChildren
親変更時に子ドキュメントのパンくずを更新

```
親ドキュメント保存完了
    ↓
parent === doc.id の子ドキュメントを検索
    ↓
ドラフト・公開両方を取得
    ↓
各子ドキュメントのパンくずを再計算して更新
```

### 2.4 循環参照防止

#### parentFilterOptions
```typescript
const parentFilterOptions: FilterOptions =
  (breadcrumbsFieldSlug = 'breadcrumbs') =>
  ({ id }) => {
    if (id) {
      return {
        id: { not_equals: id },                       // 自身を除外
        [`${breadcrumbsFieldSlug}.doc`]: { not_in: [id] }  // 子孫を除外
      }
    }
    return true
  }
```

### 2.5 親ドキュメント取得

#### getParents再帰処理
```typescript
async function getParents(req, pluginConfig, collection, doc, docs = []) {
  const parent = doc[parentSlug]

  if (parent) {
    // IDの場合: findByIDで取得
    // オブジェクトの場合: そのまま使用

    if (retrievedParent[parentSlug]) {
      // 親の親がある場合は再帰
      return getParents(..., [retrievedParent, ...docs])
    }
    return [retrievedParent, ...docs]
  }

  return docs
}
```

## 3. データモデル

### 3.1 Breadcrumb型
```typescript
type Breadcrumb = {
  doc: string       // ドキュメントID
  label: string     // 表示ラベル
  url?: string      // URL（オプション）
}
```

### 3.2 カスタム生成関数型
```typescript
type GenerateURL = (
  docs: Array<Record<string, unknown>>,  // 親ドキュメント配列
  currentDoc: Record<string, unknown>,   // 現在のドキュメント
  collection: SanitizedCollectionConfig,
  req: PayloadRequest,
) => string

type GenerateLabel = (
  docs: Array<Record<string, unknown>>,
  currentDoc: Record<string, unknown>,
  collection: SanitizedCollectionConfig,
  req: PayloadRequest,
) => string
```

### 3.3 ER図
```
+------------------+
|    Document      |
+------------------+
| id (PK)          |
| parent (FK)      |---+
| breadcrumbs[]    |   |
|   - doc (FK)     |   |
|   - url          |   |
|   - label        |   |
| ...              |   |
+------------------+   |
        ↑              |
        +--------------+
        (自己参照)
```

## 4. インターフェース仕様

### 4.1 プラグイン使用例
```typescript
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'

export default buildConfig({
  plugins: [
    nestedDocsPlugin({
      collections: ['pages', 'categories'],
      generateURL: (docs, currentDoc) =>
        docs.map(d => d.slug).join('/'),
      generateLabel: (docs, currentDoc) =>
        currentDoc.title || 'Untitled',
    }),
  ],
})
```

### 4.2 エクスポート
```typescript
// プラグイン本体
export { nestedDocsPlugin } from './index.js'

// フィールド生成ヘルパー
export { createBreadcrumbsField } from './fields/breadcrumbs.js'
export { createParentField } from './fields/parent.js'

// ユーティリティ
export { getParents } from './utilities/getParents.js'

// 型定義
export type {
  NestedDocsPluginConfig,
  Breadcrumb,
  GenerateURL,
  GenerateLabel
} from './types.js'
```

## 5. 画面設計

### 5.1 管理画面

#### ドキュメント編集画面
- サイドバーに「Parent」フィールド表示
- 本文エリアに「Breadcrumbs」配列（読み取り専用）表示
- Breadcrumbsはdoc, url, labelの行形式

### 5.2 UIフロー
```
ドキュメント編集
    ↓ [Parentを選択]
親ドキュメント選択ダイアログ
    ↓ （循環参照防止フィルタ適用）
    ↓ [保存]
パンくず自動生成
    ↓
子ドキュメントのパンくず自動更新
```

## 6. ビジネスロジック

### 6.1 パンくず生成ロジック
```typescript
// populateBreadcrumbs処理
const allParentDocuments = await getParents(...)
allParentDocuments.push(currentDocument)

const breadcrumbs = allParentDocuments.map((_, i) =>
  formatBreadcrumb({
    docs: allParentDocuments.slice(0, i + 1),
    generateLabel,
    generateURL,
  }),
)
```

### 6.2 下書き・公開対応
```typescript
// resaveChildren: 下書きと公開両方を更新
const draftChildren = initialDraftChildren.docs.filter(
  child => child._status === 'draft'
)
const publishedChildren = await req.payload.find({
  draft: false,
  ...
})
```

### 6.3 ローカライズ対応
- breadcrumbsフィールドに`localized: true`を設定
- 言語ごとに異なるパンくずリストを保持可能

## 7. エラー処理

### 7.1 子ドキュメント更新エラー
```typescript
try {
  // 子ドキュメント更新
} catch (err) {
  req.payload.logger.error(
    'Nested Docs plugin encountered an error while re-saving a child document.'
  )

  if (err?.name === ValidationErrorName) {
    throw new APIError(
      'Could not publish or save changes: One or more children are invalid.',
      400,
    )
  }
}
```

### 7.2 自己参照更新エラー
```typescript
try {
  // 自身の更新
} catch (err) {
  payload.logger.error(
    'Nested Docs plugin has had an error while adding breadcrumbs during document creation.'
  )
}
```

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

### 8.1 循環参照防止
- parentFilterOptionsにより自身と子孫を親として選択不可
- 無限ループ防止の自動フィルタリング

### 8.2 アクセス制御
- breadcrumbsフィールドはreadOnly: trueで直接編集不可
- parentフィールドのアクセス制御はコレクション設定に従う

## 9. パフォーマンス考慮事項

### 9.1 再帰取得の最適化
- getParentsは親をたどる再帰処理
- disableErrors: trueで存在しない親のエラーを抑制
- depth: 0で余分な関連データ取得を防止

### 9.2 子ドキュメント更新
- 親変更時に全子ドキュメントを更新
- 大量の子がある場合はパフォーマンス影響あり
- limit: 0で全件取得

## 10. 非機能要件対応

### 10.1 ローカライズ
- breadcrumbsフィールドはlocalized: trueで多言語対応
- 言語切り替え時に適切なパンくず表示

### 10.2 バージョン管理対応
- _status: 'draft'/'published'を考慮した更新処理
- 下書き親のときは子を更新しない

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

### 11.1 推奨読解順序

1. **型定義の理解**
   - `packages/plugin-nested-docs/src/types.ts` (1-38行目)
   - NestedDocsPluginConfig、Breadcrumb、GenerateURL、GenerateLabel型

2. **フィールド定義**
   - `packages/plugin-nested-docs/src/fields/parent.ts` (1-22行目)
   - `packages/plugin-nested-docs/src/fields/breadcrumbs.ts` (1-47行目)

3. **プラグイン本体**
   - `packages/plugin-nested-docs/src/index.ts` (15-70行目)
   - コレクション拡張とフック登録

4. **ユーティリティ関数**
   - `packages/plugin-nested-docs/src/utilities/getParents.ts` (5-46行目)
   - `packages/plugin-nested-docs/src/utilities/populateBreadcrumbs.ts` (18-63行目)

5. **フック実装**
   - `packages/plugin-nested-docs/src/hooks/populateBreadcrumbsBeforeChange.ts`
   - `packages/plugin-nested-docs/src/hooks/resaveSelfAfterCreate.ts`
   - `packages/plugin-nested-docs/src/hooks/resaveChildren.ts`

### 11.2 プログラム呼び出し階層図
```
nestedDocsPlugin(config)
├── createParentField(collection.slug)
│   └── parentFilterOptions(breadcrumbsFieldSlug)
├── createBreadcrumbsField(collection.slug)
└── return Config with hooks
    ├── beforeChange
    │   └── populateBreadcrumbsBeforeChange
    │       └── populateBreadcrumbs
    │           ├── getParents (再帰)
    │           └── formatBreadcrumb
    └── afterChange
        ├── resaveSelfAfterCreate
        │   └── payload.update()
        └── resaveChildren
            ├── payload.find() (ドラフト)
            ├── payload.find() (公開)
            └── payload.update() (各子)
                └── populateBreadcrumbs
```

### 11.3 データフロー図
```
[ドキュメント保存]
      ↓
+------------------------+
| beforeChange           |
| populateBreadcrumbs    |
+------------------------+
      ↓
+------------------------+
| getParents (再帰)       |
| 親 → 親の親 → ...       |
+------------------------+
      ↓
+------------------------+
| formatBreadcrumb       |
| 各親 → {doc,url,label} |
+------------------------+
      ↓
[DB保存]
      ↓
+------------------------+
| afterChange            |
| resaveSelfAfterCreate  |
| (create時のみ)          |
+------------------------+
      ↓
+------------------------+
| afterChange            |
| resaveChildren         |
| (子ドキュメント更新)     |
+------------------------+
```

### 11.4 関連ファイル一覧
| ファイルパス | 役割 |
|-------------|------|
| packages/plugin-nested-docs/src/index.ts | プラグインメイン実装 |
| packages/plugin-nested-docs/src/types.ts | 型定義 |
| packages/plugin-nested-docs/src/fields/parent.ts | parentフィールド生成 |
| packages/plugin-nested-docs/src/fields/breadcrumbs.ts | breadcrumbsフィールド生成 |
| packages/plugin-nested-docs/src/fields/parentFilterOptions.ts | 循環参照防止フィルタ |
| packages/plugin-nested-docs/src/hooks/populateBreadcrumbsBeforeChange.ts | パンくず生成フック |
| packages/plugin-nested-docs/src/hooks/resaveSelfAfterCreate.ts | 自己参照更新フック |
| packages/plugin-nested-docs/src/hooks/resaveChildren.ts | 子ドキュメント更新フック |
| packages/plugin-nested-docs/src/utilities/getParents.ts | 親取得ユーティリティ |
| packages/plugin-nested-docs/src/utilities/populateBreadcrumbs.ts | パンくず生成ユーティリティ |
| packages/plugin-nested-docs/src/utilities/formatBreadcrumb.ts | パンくずフォーマット |
| packages/plugin-nested-docs/package.json | パッケージ設定 |

## 12. 変更履歴

| 日付 | バージョン | 変更内容 |
|------|-----------|----------|
| 2026-03-01 | 1.0 | 初版作成 |
