# 機能設計書 10-履歴なしパッドコピー

## 概要

本ドキュメントは、Etherpad APIにおける「履歴なしパッドコピー」機能の設計仕様を記載する。

### 本機能の処理概要

既存のパッドの現在の内容のみを別のIDにコピーする機能である。リビジョン履歴を含まず、現時点のテキスト内容と書式のみがコピーされる。ストレージ効率が高く、クリーンな状態での複製に適している。

**業務上の目的・背景**：
リビジョン履歴を持たないクリーンなパッドを作成したい場合や、ストレージ容量を節約したい場合に利用される。また、履歴に含まれる機密情報を除去した状態でパッドを共有したい場合にも有効である。

**機能の利用シーン**：
- 履歴を含めずにパッドをテンプレートとして使用する場合
- ストレージ効率を重視してパッドを複製する場合
- 履歴に含まれる編集情報を除去して共有する場合
- 軽量なバックアップを作成する場合

**主要な処理内容**：
1. コピー元パッドの存在確認
2. コピー先がグループパッドの場合、グループ存在確認
3. コピー先に既存パッドがある場合の処理（forceオプション）
4. 著者情報のコピー
5. 新規パッドの作成（改行のみの初期状態）
6. 属性プールのクローン
7. 現在のテキストと属性を含むChangesetの作成・適用
8. グループパッドの場合、グループへの登録

**関連システム・外部連携**：
- ueberDB2データベースによるデータ保存
- フック機構（padCopy）

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

## 関連画面

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

## 機能種別

CRUD操作（Create） - 軽量複製処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| sourceID | string | Yes | コピー元パッドID | 存在するパッドIDであること |
| destinationID | string | Yes | コピー先パッドID | 有効なパッドIDであること |
| force | boolean | No | 既存パッドの上書きを許可 | true/false（デフォルト: false） |
| authorId | string | No | コピー操作の著者ID | 空文字列可 |
| apikey | string | Yes（APIキー認証時） | API認証キー | 設定ファイルのAPIキーと一致すること |

### 入力データソース

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

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| code | number | 処理結果コード（0: 成功） |
| message | string | 処理結果メッセージ（"ok"） |
| data.padID | string | コピー先パッドID |

### 出力先

HTTPレスポンス（JSON形式）

## 処理フロー

### 処理シーケンス

```
1. API認証
   └─ APIキーまたはOAuth2トークンを検証
2. コピー元パッド取得
   └─ getPadSafe(sourceID, true)で存在確認と取得
3. コピー元パッドをDBにフラッシュ
   └─ pad.saveToDatabase()
4. コピー先グループ確認（グループパッドの場合）
   └─ $を含むdestinationIDの場合、グループ存在確認
5. 既存パッド処理
   └─ force=trueで既存パッドがある場合は削除
   └─ force=falseで既存パッドがある場合はエラー
6. 著者情報コピー
   └─ 全著者のパッドリストにdestinationIDを追加
7. グループ登録（グループパッドの場合）
   └─ db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1)
8. 新規パッド作成
   └─ padManager.getPad(destinationID, '\n', authorId)
9. 属性プールクローン
   └─ dstPad.pool = srcPad.pool.clone()
10. Changesetの作成と適用
    └─ SmartOpAssemblerでatextからChangeset生成
    └─ dstPad.appendRevision(changeset, authorId)
11. フック実行
    └─ padCopy フックを実行
12. レスポンス返却
    └─ {padID: destinationID}
```

### フローチャート

```mermaid
flowchart TD
    A[POST /api/2/pads/copyWithoutHistory] --> B{認証検証}
    B -->|失敗| C[401 Unauthorized]
    B -->|成功| D[getPadSafe sourceID, true]
    D --> E{パッド存在?}
    E -->|No| F[400 padID does not exist]
    E -->|Yes| G[saveToDatabase]
    G --> H{destinationIDに$?}
    H -->|Yes| I[グループ存在確認]
    H -->|No| J{destinationID存在?}
    I --> K{グループ存在?}
    K -->|No| L[400 groupID does not exist]
    K -->|Yes| J
    J -->|Yes| M{force=true?}
    J -->|No| N[著者情報コピー]
    M -->|No| O[400 destinationID already exists]
    M -->|Yes| P[既存パッド削除]
    P --> N
    N --> Q{グループパッド?}
    Q -->|Yes| R[グループに登録]
    Q -->|No| S[padManager.getPad 新規]
    R --> S
    S --> T[pool.clone]
    T --> U[SmartOpAssembler]
    U --> V[appendRevision]
    V --> W[padCopy hook]
    W --> X["返却 {padID}"]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 履歴なし | リビジョン0から開始、履歴は1リビジョンのみ | 常に |
| BR-002 | 書式保持 | 属性プールをクローンして書式を維持 | 常に |
| BR-003 | テキスト保持 | 現在のテキスト内容のみコピー | 常に |
| BR-004 | 上書き制御 | force=trueの場合のみ既存パッドを上書き | 常に |
| BR-005 | 著者継承 | コピー元の著者全員にコピー先パッドを関連付け | 常に |

### 計算ロジック

- 初期パッド長: 2（"\n\n"）
- Changeset: `pack(oldLength, newLength, ops, newText)`
- 属性クローン: `srcPad.pool.clone()`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| コピー元取得 | pad:{sourceID} | SELECT | パッド本体取得 |
| グループ確認 | group:{groupID} | SELECT | グループ存在確認 |
| 既存削除 | pad:{destinationID}等 | DELETE | force時の削除 |
| パッド作成 | pad:{destinationID} | INSERT | 新規パッド作成 |
| リビジョン作成 | pad:{destinationID}:revs:0 | INSERT | 初期リビジョン |
| リビジョン追加 | pad:{destinationID}:revs:1 | INSERT | コンテンツリビジョン |
| 著者更新 | author:{authorID} | UPDATE | パッドリスト追加 |
| グループ更新 | group:{groupID} | UPDATE | padsに追加 |

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

#### pad:{destinationID}

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | atext | コピー元の現在のatext | 属性付きテキスト |
| INSERT | pool | コピー元poolのクローン | 属性プール |
| INSERT | head | 1 | リビジョン1 |
| INSERT | chatHead | -1 | チャットなし |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 1 | パラメータエラー | sourceIDが存在しない | 正しいsourceIDを指定 |
| 1 | パラメータエラー | destinationIDのグループが存在しない | 正しいgroupIDを指定 |
| 1 | パラメータエラー | destinationIDが既に存在（force=false） | force=trueまたは別のIDを指定 |
| 4 | 認証エラー | APIキーが無効 | 正しいAPIキーを指定 |
| 2 | 内部エラー | データベース操作失敗 | サーバーログを確認 |

### リトライ仕様

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

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

新規パッド作成とリビジョン追加は順次実行される。完全なトランザクション管理は行われないが、単一パッドの操作は通常アトミックに完了する。

## パフォーマンス要件

- レスポンス時間: 通常200ms以下（コピー元のサイズに依存）
- リビジョン履歴をコピーしないため、copyPadより高速

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

- APIキーまたはOAuth2トークンによる認証が必須
- コピー先グループへのアクセス権限はセッションで制御
- 履歴が除去されるため、過去の編集情報は取得不可

## 備考

- APIバージョン1.2.15から利用可能（authorIdは1.3.0から）
- チャット履歴はコピーされない
- 完全な履歴が必要な場合はcopyPad APIを使用

---

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

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

### 推奨読解順序

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

コピー処理で使用されるデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Pad.ts | `src/node/db/Pad.ts` | atext構造とpool、copyPadWithoutHistory()メソッド |
| 1-2 | SmartOpAssembler.ts | `src/static/js/SmartOpAssembler.ts` | Changeset組み立て |

**読解のコツ**: `atext`は属性付きテキストで、`text`と`attribs`を持つ。`pool`は属性プールで、著者情報や書式情報を管理。`SmartOpAssembler`でChangesetを組み立てる。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | RestAPI.ts | `src/node/handler/RestAPI.ts` | POST /pads/copyWithoutHistory のルーティング |
| 2-2 | APIHandler.ts | `src/node/handler/APIHandler.ts` | copyPadWithoutHistoryパラメータ定義 |

**主要処理フロー**:
1. **1246-1275行目（RestAPI.ts）**: POST /pads/copyWithoutHistory のルーティング定義
2. **131行目（APIHandler.ts）**: バージョン1.2.15 `copyPadWithoutHistory: ['sourceID', 'destinationID', 'force']`
3. **137行目（APIHandler.ts）**: バージョン1.3.0 `copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId']`

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

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

**主要処理フロー**:
- **633-636行目（API.ts）**: copyPadWithoutHistory関数の実装
  - **634行目**: `getPadSafe(sourceID, true)` でパッド取得
  - **635行目**: `pad.copyPadWithoutHistory(destinationID, force, authorId)` でコピー実行
- **500-555行目（Pad.ts）**: copyPadWithoutHistory関数の実装
  - **502行目**: `saveToDatabase()` でフラッシュ
  - **505行目**: `checkIfGroupExistAndReturnIt()` でグループ確認
  - **508行目**: `removePadIfForceIsTrueAndAlreadyExist()` で既存削除
  - **510行目**: `copyAuthorInfoToDestinationPad()` で著者コピー
  - **513-515行目**: グループパッドのグループ登録
  - **518行目**: `padManager.getPad(destinationID, '\n', authorId)` で新規パッド作成
  - **519行目**: `dstPad.pool = this.pool.clone()` で属性プールクローン
  - **524行目**: `SmartOpAssembler` でChangeset組み立て
  - **537行目**: `pack()` でChangeset生成
  - **538行目**: `dstPad.appendRevision(changeset, authorId)` で適用

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

```
POST /api/2/pads/copyWithoutHistory (RestAPI.ts)
    │
    ├─ APIHandler.handle() (APIHandler.ts:161)
    │      │
    │      ├─ 認証検証 (APIHandler.ts:175-200)
    │      │
    │      └─ api.copyPadWithoutHistory(sourceID, destinationID, force, authorId) (API.ts:633)
    │              │
    │              ├─ getPadSafe(sourceID, true)
    │              │
    │              └─ pad.copyPadWithoutHistory() (Pad.ts:500)
    │                      │
    │                      ├─ saveToDatabase()
    │                      │
    │                      ├─ checkIfGroupExistAndReturnIt()
    │                      │
    │                      ├─ removePadIfForceIsTrueAndAlreadyExist()
    │                      │
    │                      ├─ copyAuthorInfoToDestinationPad()
    │                      │
    │                      ├─ [グループ] db.setSub(group, pads)
    │                      │
    │                      ├─ padManager.getPad(destinationID, '\n', authorId)
    │                      │
    │                      ├─ dstPad.pool = this.pool.clone()
    │                      │
    │                      ├─ SmartOpAssembler
    │                      │      └─ opsFromAText(oldAText)
    │                      │
    │                      ├─ pack(oldLength, newLength, ops, newText)
    │                      │
    │                      ├─ dstPad.appendRevision(changeset, authorId)
    │                      │
    │                      └─ hooks.aCallAll('padCopy')
    │
    └─ Response: {code: 0, message: "ok", data: {padID}}
```

### データフロー図

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

HTTP POST                 ─────▶ RestAPI.ts
/pads/copyWithoutHistory         ルーティング
                                     │
sourceID                  ─────▶ API.ts
destinationID                    getPadSafe()
force                                │
authorId                         Pad.ts copyPadWithoutHistory()
                                 │
                                 ├─ saveToDatabase()
                                 │
                                 ├─ checkGroupExist()
                                 │
                                 ├─ [force] removePad()
                                 │
                                 ├─ copyAuthorInfo()
                                 │
                                 ├─ [グループ] グループ登録
                                 │
                                 ├─ padManager.getPad(new)
                                 │
                                 ├─ pool.clone()
                                 │
                                 ├─ SmartOpAssembler → Changeset
                                 │
                                 └─ appendRevision()
                                         │
                                         ▼
                                 {padID} ─────▶ 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` | ソース | copyPadWithoutHistory実装 |
| Pad.ts | `src/node/db/Pad.ts` | ソース | copyPadWithoutHistory実装 |
| PadManager.ts | `src/node/db/PadManager.ts` | ソース | パッド作成 |
| SmartOpAssembler.ts | `src/static/js/SmartOpAssembler.ts` | ソース | Changeset組み立て |
| Changeset.ts | `src/static/js/Changeset.ts` | ソース | pack, opsFromAText |
| AttributePool.ts | `src/static/js/AttributePool.ts` | ソース | 属性プールクローン |
| GroupManager.ts | `src/node/db/GroupManager.ts` | ソース | グループ存在確認 |
| AuthorManager.ts | `src/node/db/AuthorManager.ts` | ソース | 著者パッドリスト更新 |
| DB.ts | `src/node/db/DB.ts` | ソース | データベースアクセス層 |
