# 機能設計書 29-アセット解決

## 概要

本ドキュメントは、Decap CMSのアセット解決機能について、エントリ内のメディアパスから実際のURLを解決し、AssetProxyオブジェクトとして管理する処理の詳細を記述した機能設計書である。

### 本機能の処理概要

エントリに含まれるメディアファイルの相対パスから、表示・ダウンロード可能な実際のURLを解決する機能である。getAsset関数がパスの解決・AssetProxyの取得・必要に応じた非同期読み込みを統括し、プレビューペインや画像ウィジェットでのメディア表示を可能にする。

**業務上の目的・背景**：CMSのエントリ内に記述されたメディアパス（例: `images/photo.jpg`）は、バックエンドの種類やコレクション設定によって実際のURLが異なる。アセット解決機能は、これらの相対パスを適切なURLに変換し、エディタのプレビューやウィジェットでメディアを表示するために不可欠である。

**機能の利用シーン**：
- エントリ編集画面のプレビューペインでの画像表示
- 画像ウィジェットでのサムネイル表示
- ファイルウィジェットでのファイル参照
- Markdownプレビューでの画像描画

**主要な処理内容**：
1. getAsset関数でパスの解決とAssetProxyの取得
2. selectMediaFilePathで相対パスを絶対パスに変換
3. メモリ上のAssetProxyがある場合はそのまま返却
4. 絶対パスの場合はURL直接参照のAssetProxyを作成
5. メモリにない場合はloadAssetで非同期読み込みを開始
6. loadAssetでバックエンド / メディアライブラリからURL解決
7. boundGetAssetでコレクション・エントリにバインドされたヘルパーを提供

**関連システム・外部連携**：Git系バックエンド（ファイルURL取得）、メディアライブラリ（displayURL取得）。

**権限による制御**：特になし。認証済みユーザーのコンテキストで動作。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | メディアライブラリ画面 | 補助機能 | メディアファイルの表示URL解決。loadMediaDisplayURLアクションによるURL取得 |
| 6 | エントリ編集画面 | 補助機能 | プレビューペインでの画像表示のためのアセットURL解決 |

## 機能種別

データ変換 / URL解決

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | Collection (ImmutableMap) | Yes | コレクション定義 | 有効なコレクションであること |
| entry | EntryMap (ImmutableMap) | Yes | エントリデータ | 有効なエントリであること |
| path | string | Yes | メディアファイルの相対パス | 空文字の場合emptyAssetを返却 |
| field | EntryField | No | フィールド定義 | フィールド固有のメディアフォルダ判定に使用 |

### 入力データソース

- エントリのデータ内のメディアパス参照
- Redux Store: state.config（設定情報）
- Redux Store: state.medias（アセットプロキシキャッシュ）
- Redux Store: state.mediaLibrary（メディアファイル一覧）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| asset | AssetProxy | URL解決済みのアセットプロキシオブジェクト（url, path, field） |
| emptyAsset | AssetProxy | パスが空の場合に返却される空のSVGアセット |

### 出力先

- Redux Store: state.medias（AssetProxy追加: addAsset）
- 呼び出し元コンポーネント（プレビュー・ウィジェットでのメディア表示）

## 処理フロー

### 処理シーケンス

```
1. getAsset({collection, entry, path, field})が呼び出される
   └─ ウィジェットやプレビューコンポーネントから
2. パスが空の場合、emptyAsset（空SVG）を即座に返却
3. selectMediaFilePathで相対パスを解決済みパス（resolvedPath）に変換
4. state.medias[resolvedPath]からキャッシュを確認
   a. isLoading === true: emptyAssetを返却（読み込み中）
   b. assetが存在: そのassetを返却（キャッシュヒット）
5. キャッシュにない場合:
   a. 絶対パスの場合: createAssetProxy({path: resolvedPath, url: path})を作成しaddAsset
   b. エラー状態の場合: 元のpathでAssetProxyを作成しaddAsset
   c. 未読み込みの場合: loadAsset(resolvedPath)をディスパッチしemptyAssetを返却
6. loadAssetの処理:
   a. LOAD_ASSET_REQUESTをディスパッチ
   b. waitForMediaLibraryToLoad()でメディアライブラリの読み込み完了を待機
   c. selectMediaFileByPath(state, resolvedPath)でメディアファイルを検索
   d. ファイルが存在: getMediaDisplayURLでURLを取得 -> AssetProxy作成 -> addAsset
   e. ファイル未存在: getMediaFile(state, resolvedPath)でバックエンドからURL取得 -> AssetProxy作成 -> addAsset
   f. LOAD_ASSET_SUCCESSをディスパッチ
```

### フローチャート

```mermaid
flowchart TD
    A[getAsset呼び出し] --> B{pathが空?}
    B -->|Yes| C[emptyAsset返却]
    B -->|No| D[selectMediaFilePath]
    D --> E{medias キャッシュ確認}
    E -->|isLoading| F[emptyAsset返却]
    E -->|asset存在| G[キャッシュのasset返却]
    E -->|未読み込み| H{絶対パス?}
    H -->|Yes| I[URL直接参照AssetProxy作成]
    H -->|No| J{エラー状態?}
    J -->|Yes| K[元パスでAssetProxy作成]
    J -->|No| L[loadAsset dispatch]
    L --> M[emptyAsset返却]
    L --> N[バックグラウンド: waitForMediaLibraryToLoad]
    N --> O{selectMediaFileByPath}
    O -->|見つかった| P[getMediaDisplayURL]
    O -->|見つからない| Q[getMediaFile]
    P --> R[AssetProxy作成 + addAsset]
    Q --> R
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-29-01 | 空パスの処理 | pathが空（falsy）の場合、空のSVGアセット（emptyAsset）を即座に返却 | !path |
| BR-29-02 | キャッシュ優先 | state.medias[resolvedPath]にassetが存在する場合はそのまま返却（再読み込みしない） | asset !== undefined |
| BR-29-03 | 読み込み中の処理 | isLoadingがtrueの場合、emptyAssetを返却して重複読み込みを防止 | isLoading === true |
| BR-29-04 | 絶対パスの直接参照 | resolvedPathが絶対パスの場合、URLとして直接使用しバックエンドへの問い合わせを省略 | isAbsolutePath(resolvedPath) |
| BR-29-05 | エラー時のフォールバック | 読み込みエラー発生後は元のpathでAssetProxyを作成（元のパスにフォールバック） | error !== undefined |
| BR-29-06 | メディアライブラリ優先 | loadAsset時、まずメディアライブラリのファイルから検索し、見つからない場合にバックエンドに問い合わせ | 常時 |

### 計算ロジック

- resolvedPath: `selectMediaFilePath(state.config, collection, entry, path, field)`
- emptyAsset: `createAssetProxy({path: 'empty.svg', file: SVGファイル})`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| キャッシュ参照 | Redux Store: state.medias | SELECT | resolvedPathでAssetProxyを検索 |
| アセット追加 | Redux Store: state.medias | INSERT/UPDATE | AssetProxyをキャッシュに追加 |
| メディアファイル検索 | Redux Store: state.mediaLibrary | SELECT | selectMediaFileByPathでファイルを検索 |

### テーブル別操作詳細

#### Redux Store: state.medias

| 操作 | 項目（キー） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | medias[resolvedPath] | asset, isLoading, error | キャッシュ確認 |
| INSERT | medias[resolvedPath].asset | AssetProxy | addAssetアクション |
| UPDATE | medias[resolvedPath].isLoading | boolean | LOAD_ASSET_REQUEST/SUCCESS/FAILURE |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | URL解決失敗 | バックエンドからのURL取得失敗 | LOAD_ASSET_FAILUREディスパッチ、元パスでフォールバック |
| - | displayURL取得失敗 | メディアファイルのdisplayURL取得失敗 | getMediaFileにフォールバック |

### リトライ仕様

自動リトライは実装されていない。エラー状態のキャッシュが存在する場合、再度getAssetが呼ばれても元パスでフォールバックAssetProxyが作成される。

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

該当なし。非同期のURL解決とRedux State更新で構成される。

## パフォーマンス要件

- AssetProxyのキャッシュにより、同一パスの繰り返し解決を防止
- isLoadingフラグにより、同一パスの重複リクエストを防止
- waitForMediaLibraryToLoadによるメディアライブラリ読み込み完了の待機

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

- 絶対パスの場合はURL直接参照となるため、外部URLの参照が可能

## 備考

- emptyAssetは空のSVGファイル（`<svg xmlns="http://www.w3.org/2000/svg"></svg>`）
- boundGetAssetはcollectionとentryをバインドした簡易関数を返す
- AssetProxyのtoString()はurlを返すため、テンプレート文字列で直接使用可能
- AssetProxyのtoBase64()は画像のBase64エンコーディングを提供

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | **10-48行目**: AssetProxyクラス。url, fileObj, path, fieldプロパティ。toString()でurlを返す。toBase64()でBase64エンコーディング |

**読解のコツ**: AssetProxyはメディアファイルのURLラッパー。ファイルオブジェクトまたはURLからURLを生成し、パスとフィールド情報を保持する。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | media.ts | `packages/decap-cms-core/src/actions/media.ts` | **96-130行目**: getAsset関数。パス解決・キャッシュ確認・非同期読み込みの分岐ロジック |

**主要処理フロー**:
1. **98行目**: pathが空ならemptyAssetを返却
2. **101行目**: selectMediaFilePathで解決済みパスを取得
3. **103行目**: state.medias[resolvedPath]からキャッシュ確認
4. **104-106行目**: isLoadingならemptyAsset返却
5. **108-111行目**: assetが存在すれば返却
6. **113-116行目**: 絶対パスならURL直接参照AssetProxy
7. **118-125行目**: エラーフォールバックまたはloadAssetディスパッチ

#### Step 3: 非同期読み込みを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | media.ts | `packages/decap-cms-core/src/actions/media.ts` | **45-67行目**: loadAsset関数。メディアライブラリ待機 -> selectMediaFileByPath検索 -> getMediaDisplayURL or getMediaFile -> addAsset |

**主要処理フロー**:
1. **48行目**: loadAssetRequest(resolvedPath)ディスパッチ
2. **50行目**: waitForMediaLibraryToLoad()で待機
3. **51行目**: selectMediaFileByPathでメディアファイル検索
4. **53-56行目**: ファイルあり -> getMediaDisplayURL -> AssetProxy作成
5. **57-60行目**: ファイルなし -> getMediaFile -> AssetProxy作成
6. **62行目**: loadAssetSuccess(resolvedPath)ディスパッチ

#### Step 4: バインドヘルパーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | media.ts | `packages/decap-cms-core/src/actions/media.ts` | **83-94行目**: boundGetAsset関数。dispatch, collection, entryをバインドしたgetAssetヘルパーを返す |

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

```
getAsset({collection, entry, path, field})    [actions/media.ts:96]
    |
    +-- selectMediaFilePath()                  [reducers/entries.ts]
    |
    +-- [キャッシュヒット] -> asset返却
    |
    +-- [絶対パス] -> createAssetProxy + addAsset
    |
    +-- [未読み込み] -> loadAsset(resolvedPath)  [actions/media.ts:45]
        |
        +-- loadAssetRequest(resolvedPath)
        |
        +-- waitForMediaLibraryToLoad()          [actions/mediaLibrary.ts:510]
        |
        +-- selectMediaFileByPath(state, path)   [reducers/mediaLibrary.ts:282]
        |
        +-- [ファイルあり] getMediaDisplayURL()  [actions/mediaLibrary.ts:522]
        |   +-- createAssetProxy() + addAsset()
        |
        +-- [ファイルなし] getMediaFile()        [actions/mediaLibrary.ts:368]
        |   +-- backend.getMediaFile(path)
        |   +-- createAssetProxy() + addAsset()
        |
        +-- loadAssetSuccess(resolvedPath)
```

### データフロー図

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

メディアパス           selectMediaFilePath              AssetProxy
(相対パス)                |                               (url + path)
        |                 v
        +---------> resolvedPath計算                     Redux Store
                         |                               (medias キャッシュ)
コレクション/          キャッシュ確認
エントリ情報               |
        |            +----+----+
        |            |    |    |
        +-------> hit  loading  miss
                   |     |      |
                  返却  empty  loadAsset
                               |
                          +----+----+
                          |         |
                     mediaFile   backend
                     displayURL  getMediaFile
                          |         |
                          +--> AssetProxy作成 -> addAsset
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| media.ts | `packages/decap-cms-core/src/actions/media.ts` | ソース | getAsset, loadAsset, boundGetAsset関数 |
| AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | ソース | AssetProxyクラス |
| mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | ソース | getMediaFile, getMediaDisplayURL, waitForMediaLibraryToLoad |
| mediaLibrary.ts | `packages/decap-cms-core/src/reducers/mediaLibrary.ts` | ソース | selectMediaFileByPath, selectMediaDisplayURL |
| entries.ts | `packages/decap-cms-core/src/reducers/entries.ts` | ソース | selectMediaFilePath関数 |
