# 機能設計書 2-グループ条件付き作成

## 概要

本ドキュメントは、Etherpad APIにおける「グループ条件付き作成」機能の設計仕様を記載する。

### 本機能の処理概要

指定されたマッパー（外部システムの識別子）に対応するグループが存在しない場合のみ新規グループを作成し、既存の場合はそのグループIDを返す機能である。これにより、外部システムとEtherpadのグループを一意に紐付けることができる。

**業務上の目的・背景**：
外部システム（LMS、CMS、プロジェクト管理ツール等）とEtherpadを統合する際、外部システム側の識別子（ユーザーID、プロジェクトID等）に基づいてグループを管理したいケースがある。本機能により、外部システムのIDをマッパーとして指定することで、冪等性を保ちながらグループを作成・取得できる。重複作成を防ぎ、システム間の整合性を維持する。

**機能の利用シーン**：
- LMSからコースIDに紐づくグループを作成・取得する場合
- シングルサインオン連携で組織IDに基づくグループを管理する場合
- 外部APIからの呼び出しで毎回同じグループを確実に取得したい場合
- マイクロサービス間でグループの一意性を保証したい場合

**主要な処理内容**：
1. 入力パラメータ（groupMapper）の型バリデーション
2. mapper2group:{groupMapper}レコードからグループID検索
3. グループIDが存在し、かつグループ実体が存在すればそのIDを返却
4. グループが存在しない場合は新規作成
5. マッパーとグループIDの関連付けをデータベースに保存
6. グループレコードにマッピング情報を記録

**関連システム・外部連携**：
- ueberDB2データベースによるデータ永続化
- 外部システムとのマッピング連携

**権限による制御**：
APIキーまたはOAuth2トークンによる認証が必要。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 画面からの直接操作はなく、API経由でのみ利用される |

## 機能種別

CRUD操作（Create/Read）- 条件付き作成パターン

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| groupMapper | string | Yes | 外部システムの識別子 | 文字列型であること |
| apikey | string | Yes（APIキー認証時） | API認証キー | 設定ファイルのAPIキーと一致すること |
| authorization | string | Yes（OAuth2認証時） | Bearerトークン | 有効なJWTトークンであること |

### 入力データソース

HTTP POSTリクエスト（`/api/2/groups/createIfNotExistsFor`）のボディから取得。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| code | number | 処理結果コード（0: 成功） |
| message | string | 処理結果メッセージ（"ok"） |
| data.groupID | string | グループID（新規作成または既存） |

### 出力先

HTTPレスポンス（JSON形式）

## 処理フロー

### 処理シーケンス

```
1. API認証
   └─ APIキーまたはOAuth2トークンを検証
2. パラメータバリデーション
   └─ groupMapperが文字列型かチェック
   └─ 文字列でなければエラー返却
3. マッピング検索
   └─ db.get(`mapper2group:${groupMapper}`)
4. グループ存在確認
   └─ groupIDが取得でき、かつグループが存在する場合
      └─ 既存のgroupIDを返却
5. グループ新規作成（存在しない場合）
   └─ createGroup()を呼び出し
6. マッピング保存
   └─ db.set(`mapper2group:${groupMapper}`, groupID)
   └─ db.setSub(`group:${groupID}`, ['mappings', groupMapper], 1)
7. レスポンス返却
   └─ {groupID: "g.xxxxxxxxxxxxxxxx"} を返却
```

### フローチャート

```mermaid
flowchart TD
    A[POST /api/2/groups/createIfNotExistsFor] --> B{認証検証}
    B -->|失敗| C[401 Unauthorized]
    B -->|成功| D{groupMapperは文字列?}
    D -->|No| E[400 Bad Request<br/>groupMapper is not a string]
    D -->|Yes| F["db.get mapper2group:groupMapper"]
    F --> G{groupID取得成功?}
    G -->|Yes| H{グループ実体存在?}
    H -->|Yes| I["返却 {groupID} 既存"]
    G -->|No| J[createGroup 実行]
    H -->|No| J
    J --> K["db.set mapper2group:groupMapper, groupID"]
    K --> L["db.setSub group:groupID<br/>mappings/groupMapper, 1"]
    L --> M["返却 {groupID} 新規"]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 冪等性保証 | 同じgroupMapperで呼び出すと常に同じgroupIDが返る | マッピングが存在する場合 |
| BR-002 | グループ実体検証 | マッピングが存在してもグループ実体がなければ再作成 | マッピングレコードのみ残存時 |
| BR-003 | 双方向マッピング | mapper2groupとgroup内mappingsの両方に記録 | 新規作成時 |

### 計算ロジック

- マッピングキー: `mapper2group:${groupMapper}`
- グループ内マッピングパス: `group:${groupID}.mappings.${groupMapper}`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| マッピング検索 | mapper2group:{groupMapper} | SELECT | 既存マッピング確認 |
| グループ存在確認 | group:{groupID} | SELECT | グループ実体確認 |
| グループ作成 | group:{groupID} | INSERT | 新規グループ作成 |
| グループ一覧追加 | groups | UPDATE | グループ一覧更新 |
| マッピング作成 | mapper2group:{groupMapper} | INSERT | マッピング保存 |
| グループマッピング追加 | group:{groupID} | UPDATE | グループ内にマッピング情報追加 |

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

#### mapper2group:{groupMapper}

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | value | 全体 | グループID取得 |
| INSERT | value | groupID | マッピング作成 |

#### group:{groupID}

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | 存在確認 | グループ実体確認 |
| UPDATE | mappings.{groupMapper} | 1 | マッピング情報追加 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 1 | パラメータエラー | groupMapperが文字列でない | 文字列を指定 |
| 4 | 認証エラー | APIキーが無効または未指定 | 正しいAPIキーを指定 |
| 2 | 内部エラー | データベース操作失敗 | サーバーログを確認 |

### リトライ仕様

データベース操作失敗時のリトライはueberDB2層で処理される。

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

グループ作成とマッピング保存は以下の順序で実行される：
1. createGroup()でグループ作成
2. Promise.allで以下を並列実行：
   - mapper2groupレコード作成
   - グループ内mappingsへの追加

アトミック性は保証されないが、冪等性により再実行で整合性が回復する。

## パフォーマンス要件

- レスポンス時間: 新規作成時200ms以下、既存取得時100ms以下
- スループット: データベースバックエンドに依存

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

- APIキーまたはOAuth2トークンによる認証が必須
- groupMapperは外部システムの識別子のため、予測可能でも問題ない
- グループIDは推測困難なランダム文字列

## 備考

- マッピングは複数設定可能（コア機能では単一だが、プラグインで拡張可能）
- グループ削除時にマッピングも自動削除される

---

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

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

### 推奨読解順序

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

マッピングデータの構造を理解することが最初のステップである。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | GroupManager.ts | `src/node/db/GroupManager.ts` | mappings構造とmapper2groupレコード形式 |

**読解のコツ**: 2つのデータ構造が関連する：(1) `mapper2group:{mapper}` はマッパーからグループIDへの逆引き用、(2) `group:{groupID}.mappings` はグループが持つマッパー一覧（削除時のクリーンアップ用）。

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

APIリクエストの入口となるファイルを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | RestAPI.ts | `src/node/handler/RestAPI.ts` | POST /groups/createIfNotExistsFor のルーティング |
| 2-2 | APIHandler.ts | `src/node/handler/APIHandler.ts` | groupMapperパラメータの抽出 |

**主要処理フロー**:
1. **329-348行目（RestAPI.ts）**: POST /groups/createIfNotExistsFor のルーティングとスキーマ定義
2. **37行目（APIHandler.ts）**: `createGroupIfNotExistsFor: ['groupMapper']` パラメータ定義

#### Step 3: コアロジックを理解する

条件付き作成の中核処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | API.ts | `src/node/db/API.ts` | createGroupIfNotExistsFor関数のエクスポート |
| 3-2 | GroupManager.ts | `src/node/db/GroupManager.ts` | createGroupIfNotExistsFor関数の実装 |

**主要処理フロー**:
- **46行目（API.ts）**: `exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;`
- **111-127行目（GroupManager.ts）**: createGroupIfNotExistsFor関数の実装
  - **112-114行目**: 型バリデーション
  - **115行目**: `db.get(\`mapper2group:${groupMapper}\`)` でマッピング検索
  - **116行目**: グループ存在確認と既存ID返却
  - **117行目**: 新規グループ作成
  - **118-125行目**: マッピング保存（並列実行）

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

```
POST /api/2/groups/createIfNotExistsFor (RestAPI.ts)
    │
    ├─ APIHandler.handle() (APIHandler.ts:161)
    │      │
    │      ├─ 認証検証 (APIHandler.ts:175-200)
    │      │
    │      └─ api.createGroupIfNotExistsFor(groupMapper) (API.ts:46)
    │              │
    │              └─ groupManager.createGroupIfNotExistsFor() (GroupManager.ts:111)
    │                      │
    │                      ├─ db.get(`mapper2group:${groupMapper}`)
    │                      │
    │                      ├─ doesGroupExist(groupID) (GroupManager.ts:85)
    │                      │      └─ db.get(`group:${groupID}`)
    │                      │
    │                      ├─ createGroup() (GroupManager.ts:96) [条件付き]
    │                      │
    │                      └─ Promise.all [条件付き]
    │                              ├─ db.set(`mapper2group:${groupMapper}`)
    │                              └─ db.setSub(`group:${groupID}`, ['mappings', groupMapper])
    │
    └─ Response: {code: 0, message: "ok", data: {groupID}}
```

### データフロー図

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

HTTP POST             ─────▶ RestAPI.ts
/groups/createIfNotExistsFor  ルーティング
                                  │
groupMapper           ─────▶ APIHandler.ts
apikey/token                 認証・パラメータ抽出
                                  │
                              GroupManager.ts
                              │
                              ├─ db.get(mapper2group:xxx)
                              │     │
                              │     ├─[存在] ─▶ doesGroupExist()
                              │     │              │
                              │     │              ├─[存在] ─▶ 既存ID返却
                              │     │              └─[不在] ─┐
                              │     └─[不在] ───────────────┘
                              │                              │
                              │                              ▼
                              │                    createGroup()
                              │                    db.set(mapper2group)
                              │                    db.setSub(mappings)
                              │
                              ▼
                          {groupID} ─────▶ JSON Response
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| RestAPI.ts | `src/node/handler/RestAPI.ts` | ソース | APIルーティング定義 |
| APIHandler.ts | `src/node/handler/APIHandler.ts` | ソース | API認証・パラメータ処理 |
| API.ts | `src/node/db/API.ts` | ソース | API関数エクスポート |
| GroupManager.ts | `src/node/db/GroupManager.ts` | ソース | グループ管理コアロジック |
| DB.ts | `src/node/db/DB.ts` | ソース | データベースアクセス層 |
