# 機能設計書 60-アプリ保存

## 概要

本ドキュメントは、GiselleワークフローをアプリケーションとしてGiselleストレージに保存・公開する機能について記述する。

### 本機能の処理概要

Giselleワークフローをアプリケーション（App）として保存し、外部から実行可能な形式で公開する機能である。ワークフロー内のApp Entryノードと終了ノードを接続することで、入力パラメータと出力を定義した実行可能なアプリとして登録される。新規作成と更新の両方に対応し、アプリの接続状態（connected/disconnected）の変更も管理する。

**業務上の目的・背景**：Giselleで設計したワークフローを再利用可能なアプリケーションとして公開することで、他のユーザーやシステムから利用可能にする。これにより、ワークフローの配布、API経由での実行、プレイグラウンドでのテスト実行などが可能となり、ワークフローの活用範囲が広がる。

**機能の利用シーン**：
- 設計したワークフローを他のチームメンバーに公開
- APIエンドポイントとしてワークフローを提供
- プレイグラウンドでテスト実行可能なアプリを作成
- ワークフローのバージョン管理（更新保存）

**主要な処理内容**：
1. アプリデータ（App）のバリデーション
2. 既存アプリの存在確認
3. ストレージへのアプリ情報保存
4. 新規作成時のコールバック実行
5. 接続状態変更イベントの発火

**関連システム・外部連携**：Giselle Storage

**権限による制御**：ワークスペースへの書き込み権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 17 | ワークフローエディタ画面 | 主画面 | アプリとしての保存・公開処理 |

## 機能種別

CRUD操作 / データ永続化

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| app | App | Yes | 保存するアプリオブジェクト | Appスキーマに準拠 |
| app.id | AppId | Yes | アプリID | app_xxxの形式 |
| app.version | "v1" | Yes | バージョン（固定） | "v1"のみ |
| app.state | "connected" \| "disconnected" | Yes | 接続状態 | 列挙型 |
| app.description | string | Yes | アプリの説明 | - |
| app.parameters | AppParameter[] | Yes | 入力パラメータ定義 | 配列 |
| app.entryNodeId | NodeId | Yes | エントリーノードID | nd_xxxの形式 |
| app.endNodeId | NodeId | 条件付き | 終了ノードID（connected時必須） | nd_xxxの形式 |
| app.workspaceId | WorkspaceId | Yes | ワークスペースID | ws_xxxの形式 |

### 入力データソース

ワークフローデザイナーUIまたはAPIからの入力

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| なし | void | 処理完了時は戻り値なし |

### 出力先

Giselleストレージ（`apps/{appId}.json`パス）

## 処理フロー

### 処理シーケンス

```
1. 入力Appデータのバリデーション
   └─ Zodスキーマで検証
2. 既存アプリの存在確認
   └─ storage.exists()で確認
3. 既存アプリデータの取得（存在する場合）
   └─ 接続状態変更イベント判定のため
4. ストレージへの保存
   └─ storage.setJson()でJSON形式で保存
5. コールバック実行
   └─ 新規作成時: appCreate
   └─ 接続状態変更時: appConnectionChange
```

### フローチャート

```mermaid
flowchart TD
    A[saveApp呼び出し] --> B[入力バリデーション]
    B --> C{既存アプリ存在?}
    C -->|Yes| D[既存アプリ取得]
    C -->|No| E[新規フラグ設定]
    D --> F[ストレージ保存]
    E --> F
    F --> G{新規作成?}
    G -->|Yes| H[appCreateコールバック]
    G -->|No| I{接続状態変更?}
    H --> I
    I -->|Yes| J[appConnectionChangeコールバック]
    I -->|No| K[処理完了]
    J --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-60-01 | App状態 | Appはconnected（接続済み）またはdisconnected（未接続）の状態を持つ | 常時 |
| BR-60-02 | 終了ノード必須 | connected状態のアプリはendNodeIdが必須 | state=connected時 |
| BR-60-03 | 新規作成コールバック | 新規アプリ作成時にappCreateコールバックを実行 | exist=false時 |
| BR-60-04 | 接続状態変更検出 | 接続状態、entryNodeId、endNodeId、workspaceIdの変更を検出 | 更新時 |
| BR-60-05 | バージョン固定 | 現在のAppスキーマバージョンは"v1"固定 | 常時 |
| BR-60-06 | 保存パス | `apps/{appId}.json`パスに保存 | 常時 |

### 計算ロジック

#### 接続状態変更イベント判定ロジック

```typescript
// 接続状態変更イベントの発火条件
if (nextApp.state === "connected") {
  // 以下のいずれかの場合にconnectedイベント発火
  - 既存アプリがない（新規作成）
  - 以前がconnected以外の状態だった
  - endNodeIdが変更された
  - entryNodeIdが変更された
  - workspaceIdが変更された
}

if (previousApp.state === "connected" && nextApp.state !== "connected") {
  // disconnectedイベント発火
}
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| アプリ保存 | - | ストレージ書き込み | JSONファイルとしてストレージに保存 |

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

本機能はデータベーステーブルではなく、Giselleストレージ（ファイルシステムまたはオブジェクトストレージ）に対して操作を行う。

#### ストレージパス

| 操作 | パス | 形式 |
|-----|------|------|
| アプリ保存 | `apps/{appId}.json` | JSON |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| VALIDATION_ERROR | バリデーションエラー | Appスキーマに準拠しないデータ | エラーメッセージを出力 |
| STORAGE_ERROR | ストレージエラー | ストレージへの書き込み失敗 | リトライまたはエラー通知 |
| CALLBACK_ERROR | コールバックエラー | コールバック関数でのエラー | エラーログ出力 |

### リトライ仕様

- ストレージエラー時は最大3回までリトライ

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

ストレージへの書き込みとコールバック実行は非トランザクション。コールバック失敗時もストレージへの保存は完了している状態となる。

## パフォーマンス要件

- 保存処理: 1秒以内
- 入力バリデーション: 10ms以内

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

- ワークスペースへの書き込み権限チェックが必要
- AppIdはシステム生成のため改ざん不可
- コールバック関数は信頼できるコンテキストで実行

## 備考

- AppのバージョンはDeprecatedAppスキーマからの移行を経て現在"v1"
- DeprecatedApp形式のデータはpreprocessで自動変換される
- connected状態はendNodeIdとentryNodeIdの両方が有効に設定されている状態を示す

---

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

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

### 推奨読解順序

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

まず、Appのデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app.ts | `packages/protocol/src/app/app.ts` | App型の定義（DisconnectedApp、ConnectedApp） |

**読解のコツ**: `z.discriminatedUnion("state", [DisconnectedApp, ConnectedApp])`により、state属性で型が判別される。ConnectedAppにはendNodeIdが必須。

**主要処理フロー**:
- **7-12行目**: AppParameterType列挙型定義
- **18-24行目**: AppParameter型定義
- **26-35行目**: DisconnectedApp型定義（endNodeId不要）
- **37-47行目**: ConnectedApp型定義（endNodeId必須）
- **61-71行目**: App型定義（discriminatedUnion）

#### Step 2: 保存処理を理解する

アプリ保存のメイン処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | save-app.ts | `packages/giselle/src/apps/save-app.ts` | saveApp関数の処理フロー |

**主要処理フロー**:
- **7-8行目**: 入力スキーマ定義（App型）
- **9-10行目**: 既存アプリの存在確認
- **12-17行目**: 既存アプリデータの取得（接続変更検出用）
- **19-23行目**: ストレージへの保存
- **24-26行目**: 新規作成時のappCreateコールバック
- **28-35行目**: 接続状態変更イベントの発火

#### Step 3: 接続状態変更イベント計算を理解する

接続状態変更の検出ロジックを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | compute-app-connection-change-event.ts | `packages/giselle/src/apps/compute-app-connection-change-event.ts` | 接続状態変更の判定ロジック |

**主要処理フロー**:
- **11-28行目**: connected状態への変更検出
  - 新規作成（!exist）
  - 状態変更（previousApp.state !== "connected"）
  - endNodeId変更
  - entryNodeId変更
  - workspaceId変更
- **30-38行目**: disconnected状態への変更検出

#### Step 4: パス生成を理解する

ストレージパスの生成方法を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | path.ts | `packages/giselle/src/path.ts` | appPath関数 |

**主要処理フロー**:
- **16-18行目**: `appPath`関数 - `apps/{appId}.json`形式のパス生成

#### Step 5: Giselle関数ユーティリティを理解する

関数定義パターンを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | create-giselle-function.ts | `packages/giselle/src/utils/create-giselle-function.ts` | createGiselleFunction関数 |

**主要処理フロー**:
- **19-43行目**: createGiselleFunction - 入力スキーマとハンドラーを組み合わせて関数を生成
- **49-71行目**: bindGiselleFunction - コンテキストをバインドしてメソッド化

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

```
Giselle.saveApp
    │
    ├─ bindGiselleFunction (context binding)
    │
    └─ saveApp (packages/giselle/src/apps/save-app.ts)
           │
           ├─ context.storage.exists
           │     └─ 既存アプリ確認
           │
           ├─ context.storage.getJson
           │     └─ 既存アプリ取得（存在時）
           │
           ├─ context.storage.setJson
           │     └─ アプリ保存
           │
           ├─ context.callbacks.appCreate
           │     └─ 新規作成コールバック（新規時）
           │
           └─ computeAppConnectionChangeEvent
                 │
                 └─ context.callbacks.appConnectionChange
                       └─ 接続状態変更コールバック
```

### データフロー図

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

App オブジェクト    ───▶ saveApp                    ───▶ ストレージ
                         │                              (apps/{id}.json)
                         ├─ バリデーション
                         ├─ 既存確認
                         ├─ 保存
                         └─ コールバック実行

App:                     処理:                            コールバック:
- id                     - Zodスキーマ検証               - appCreate
- state                  - 存在確認                      - appConnectionChange
- description            - JSON保存
- parameters             - イベント計算
- entryNodeId
- endNodeId?
- workspaceId
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| save-app.ts | `packages/giselle/src/apps/save-app.ts` | ソース | アプリ保存メイン処理 |
| app.ts | `packages/protocol/src/app/app.ts` | ソース | App型定義 |
| compute-app-connection-change-event.ts | `packages/giselle/src/apps/compute-app-connection-change-event.ts` | ソース | 接続状態変更イベント計算 |
| types.ts | `packages/giselle/src/apps/types.ts` | ソース | コールバック型定義 |
| path.ts | `packages/giselle/src/path.ts` | ソース | ストレージパス生成 |
| create-giselle-function.ts | `packages/giselle/src/utils/create-giselle-function.ts` | ソース | 関数ユーティリティ |
| giselle.ts | `packages/giselle/src/giselle.ts` | ソース | Giselleメインクラス |
