# 機能設計書 96-モジュール間リンク

## 概要

本ドキュメントは、Medusaコマースプラットフォームにおけるモジュール間リンク機能の設計仕様を記述する。モジュール間リンク機能は、異なるモジュール間のエンティティを関連付け、モジュール間の疎結合を維持しながらデータ連携を可能にする。

### 本機能の処理概要

モジュール間リンク機能は、異なるドメインモジュール（商品、注文、顧客など）のエンティティ間の関連を管理するリンクテーブルを提供する。各モジュールは独立して動作しながら、リンクモジュールを通じて他のモジュールのエンティティと関連付けることができる。これにより、マイクロサービス的なアーキテクチャパターンを実現する。

**業務上の目的・背景**：ECプラットフォームでは、商品と在庫、注文と顧客、カートと配送オプションなど、様々なエンティティ間の関連が必要である。従来のモノリシックなアプローチでは外部キー制約を使用するが、Medusaのモジュラーアーキテクチャでは各モジュールが独立したデータベーススキーマを持つ。リンクモジュールは、これらの独立したモジュール間の関連を管理し、柔軟なカスタマイズと拡張を可能にする。

**機能の利用シーン**：
- 商品とその在庫アイテムの関連付け
- 注文と顧客の関連付け
- カートと配送オプションの関連付け
- 販売チャネルと商品の関連付け
- カスタムモジュールと既存モジュールの連携

**主要な処理内容**：
1. リンクの作成（create）- エンティティ間の関連を作成
2. リンクの取得（retrieve/list）- 関連情報の取得
3. リンクの解除（dismiss）- 関連のソフトデリート
4. リンクの削除（delete）- 関連のハードデリート
5. リンクの復元（restore）- ソフトデリート済みリンクの復元

**関連システム・外部連携**：
- PostgreSQL：リンクデータの永続化
- イベントバス：リンク作成/削除時のイベント発行

**権限による制御**：リンク操作自体には権限制御は設けられていない。リンクを操作するサービス層で適切な認可を実装する。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は直接UIを持たないバックエンド機能 |

## 機能種別

CRUD操作 / データ連携

## 入力仕様

### 入力パラメータ（create - 単一）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| primaryKeyOrBulkData | string \| string[] | Yes | 主キーデータ | - |
| foreignKeyData | string | Yes | 外部キーデータ | - |
| extraFields | Record<string, unknown> | No | 追加フィールド | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（create - 一括）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| primaryKeyOrBulkData | [string \| string[], string, Record<string, unknown>][] | Yes | 一括作成データ配列 | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（retrieve）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| primaryKeyData | string \| string[] | Yes | 主キーデータ | - |
| foreignKeyData | string | Yes | 外部キーデータ | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（list）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| filters | Record<string, unknown> | No | フィルタ条件 | 有効なキー名のみ |
| config | FindConfig<unknown> | No | ページネーション・ソート設定 | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力データソース

- サービス層からの直接呼び出し
- ワークフローステップ内からの呼び出し
- APIルートハンドラーからの呼び出し

## 出力仕様

### 出力データ（create）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| links | unknown[] | 作成されたリンクレコードの配列 |

### 出力データ（retrieve）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| link | unknown | 取得されたリンクレコード |

### 出力データ（list）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| links | unknown[] | リンクレコードの配列 |

### 出力先

- リンクデータ：PostgreSQLデータベース（リンク専用テーブル）

## 処理フロー

### 処理シーケンス（create）

```
1. データ構築
   └─ buildData()で主キー・外部キー・追加フィールドを結合

2. 一括処理判定
   └─ foreignKeyDataがundefinedの場合は一括処理

3. リンク作成
   └─ linkService_.create()でDBに保存

4. イベント発行
   └─ ATTACHED イベントを発行

5. シリアライズ
   └─ baseRepository_.serialize()で返却形式に変換
```

### フローチャート

```mermaid
flowchart TD
    A[create呼び出し] --> B{foreignKeyData undefined?}
    B -->|Yes| C[一括処理モード]
    B -->|No| D[単一処理モード]
    C --> E[配列をループしてbuildData]
    D --> F[buildDataで単一レコード構築]
    E --> G[linkService_.create]
    F --> G
    G --> H[ATTACHED イベント発行]
    H --> I[serialize して返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-96-1 | 複合主キー対応 | primaryKey_が配列の場合、全要素を結合してキーとする | 複合主キー定義時 |
| BR-96-2 | フィールド検証 | 入力データのキー名が primaryKey_ または foreignKey_ に含まれることを確認 | validate時 |
| BR-96-3 | ソフトデリート | dismissはソフトデリートを実行し、物理削除しない | dismiss呼び出し時 |
| BR-96-4 | イベント発行 | create/restoreはATTACHED、dismiss/delete/softDeleteはDETACHEDイベントを発行 | 各操作完了時 |
| BR-96-5 | take未指定時の挙動 | config.takeが未指定の場合はnullを設定（制限なし） | list/listAndCount時 |

### 計算ロジック

主キーの結合：
```typescript
// 複合主キーの結合
const pk = this.primaryKey_.join(",")
// 例: ["product_id", "variant_id"] → "product_id,variant_id"

// データ構築
return {
  [pk]: primaryKeyData,  // 複合キー値を配列で設定
  [this.foreignKey_]: foreignKeyData,
  ...extra
}
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| create | link_{entity_name} | INSERT | リンクレコードの作成 |
| retrieve | link_{entity_name} | SELECT | 単一リンクの取得 |
| list | link_{entity_name} | SELECT | リンク一覧の取得 |
| listAndCount | link_{entity_name} | SELECT + COUNT | リンク一覧とカウントの取得 |
| dismiss | link_{entity_name} | UPDATE (soft delete) | ソフトデリート |
| delete | link_{entity_name} | DELETE | ハードデリート |
| softDelete | link_{entity_name} | UPDATE | ソフトデリート |
| restore | link_{entity_name} | UPDATE | ソフトデリート解除 |

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

#### link_{entity_name}（動的テーブル名）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 | - |
| INSERT | {primary_key} | 主キー値 | カラム名はリンク定義による |
| INSERT | {foreign_key} | 外部キー値 | カラム名はリンク定義による |
| INSERT | {extra_fields} | 追加フィールド値 | リンク定義による |
| SELECT | - | primaryKey + foreignKey でフィルタ | - |
| UPDATE | deleted_at | 現在時刻 | ソフトデリート時 |
| UPDATE | deleted_at | null | リストア時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| NOT_FOUND | MedusaError | retrieve時にエントリが存在しない | 正しいキーを指定 |
| INVALID_DATA | MedusaError | 複合主キーのデータ長が不一致 | 正しい数の値を配列で指定 |
| INVALID_DATA | MedusaError | 無効なフィールド名を指定 | primaryKey_またはforeignKey_に含まれるキーのみ使用 |

### リトライ仕様

- データベース操作のリトライはフレームワーク層で管理
- トランザクション失敗時は自動ロールバック

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

- create/dismiss/delete/softDelete/restore は @InjectTransactionManager デコレータでトランザクション管理
- 複数レコードの操作は同一トランザクション内で実行
- イベント発行はトランザクションコミット後に @EmitEvents デコレータで処理

## パフォーマンス要件

- 単一リンク取得：10ms以内
- リンク一覧取得：ページネーション適用時100ms以内
- 一括作成：バッチサイズに比例

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

- リンクデータに機密情報を含めないこと
- 入力データのバリデーションは validateFields() で実施
- イベント発行時は internal: true オプションでログを抑制可能

## 備考

- リンクモジュールは動的に生成され、テーブル名やカラム名はリンク定義による
- ModuleJoinerConfig を通じてリンク関係が定義される
- extraFields でリンク固有のメタデータを保存可能

---

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

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

### 推奨読解順序

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

リンクモジュールの依存関係と内部構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | link-module-service.ts | `packages/modules/link-modules/src/services/link-module-service.ts` | InjectedDependenciesの構造 |

**読解のコツ**: primaryKey_は配列（複合主キー対応）、foreignKey_は文字列で定義されている。extraFields_はリンク固有の追加カラムを保持する。

#### Step 2: モジュールサービスを理解する

リンクモジュールサービスの実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | link-module-service.ts | `packages/modules/link-modules/src/services/link-module-service.ts` | LinkModuleServiceの実装 |

**主要処理フロー**:
- **49-70行目**: コンストラクタでDI、primaryKey_を配列に正規化
- **76-99行目**: buildData()で主キー・外部キー・追加フィールドを結合
- **105-118行目**: validateFields()でフィールド名を検証
- **120-141行目**: retrieve()で単一リンクを取得
- **143-156行目**: list()でリンク一覧を取得
- **179-224行目**: create()でリンクを作成、イベント発行

#### Step 3: 下位サービスを理解する

LinkServiceの実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | link.ts | `packages/modules/link-modules/src/services/link.ts` | LinkServiceの実装 |

**主要処理フロー**:
- **21-32行目**: list()でフィルタ付きリストを取得
- **44-52行目**: create()でレコードを作成
- **54-76行目**: dismiss()でソフトデリート
- **78-86行目**: delete()でハードデリート
- **88-110行目**: softDelete()でフィルタ付きソフトデリート
- **112-134行目**: restore()でリストア

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

```
LinkModuleService implements ILinkModule
    │
    ├─ create(primaryKeyOrBulkData, foreignKeyData, extraFields, sharedContext)
    │      ├─ [一括モード] for ([pk, fk, extra] of data)
    │      │      └─ buildData(pk, fk, extra)
    │      │
    │      ├─ [単一モード] buildData(pk, fk, extraFields)
    │      │
    │      ├─ linkService_.create(data, sharedContext)
    │      │
    │      ├─ moduleEventBuilderFactory({ action: ATTACHED })
    │      │
    │      └─ baseRepository_.serialize(links)
    │
    ├─ retrieve(primaryKeyData, foreignKeyData, sharedContext)
    │      ├─ buildData(pk, fk)
    │      ├─ ModulesSdkUtils.buildQuery(filter)
    │      ├─ linkService_.list(queryOptions, {}, sharedContext)
    │      └─ baseRepository_.serialize(entry[0])
    │
    ├─ list(filters, config, sharedContext)
    │      ├─ config.take ??= null
    │      ├─ linkService_.list(filters, config, sharedContext)
    │      └─ baseRepository_.serialize(rows)
    │
    ├─ dismiss(primaryKeyOrBulkData, foreignKeyData, sharedContext)
    │      ├─ buildData()
    │      ├─ linkService_.dismiss(data, sharedContext)
    │      ├─ moduleEventBuilderFactory({ action: DETACHED })
    │      └─ baseRepository_.serialize(links)
    │
    ├─ delete(data, sharedContext)
    │      ├─ validateFields(data)
    │      ├─ linkService_.delete(data, sharedContext)
    │      └─ moduleEventBuilderFactory({ action: DETACHED })
    │
    ├─ softDelete(data, { returnLinkableKeys }, sharedContext)
    │      ├─ validateFields(inputArray)
    │      ├─ softDelete_(inputArray, sharedContext)
    │      ├─ mapObjectTo(cascadedEntitiesMap, entityNameToLinkableKeysMap)
    │      └─ moduleEventBuilderFactory({ action: DETACHED })
    │
    └─ restore(data, { returnLinkableKeys }, sharedContext)
           ├─ validateFields(inputArray)
           ├─ restore_(inputArray, sharedContext)
           ├─ mapObjectTo(cascadedEntitiesMap, entityNameToLinkableKeysMap)
           └─ moduleEventBuilderFactory({ action: ATTACHED })
```

### データフロー図

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

create(pk, fk, extra)
    │
    ▼
buildData() ─────────────────▶ { pk: value, fk: value, ...extra }
    │
    ▼
linkService_.create()
    │
    ▼
linkRepository_.create() ────▶ [PostgreSQL]
    │                             link_{entity}テーブル
    ▼
moduleEventBuilderFactory()
    │
    ▼
eventBusModuleService_.emit() ──▶ {entity}.attached イベント
    │
    ▼
baseRepository_.serialize() ──▶ 作成されたリンクレコード
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| link-module-service.ts | `packages/modules/link-modules/src/services/link-module-service.ts` | ソース | リンクモジュールサービス |
| link.ts | `packages/modules/link-modules/src/services/link.ts` | ソース | リンクサービス（リポジトリ操作） |
| ILinkModule | `packages/core/types/src/link-modules/index.ts` | 型定義 | ILinkModuleインターフェース |
| ModuleJoinerConfig | `packages/core/types/src/joiner/index.ts` | 型定義 | リンク定義設定 |
