# server-data — フォーム内 JS からサーバ側 Notes データを読み取る全経路

**Scope:** Form.io フォームの JS 式 (`customConditional` / `calculateValue` / `customDefaultValue` / `validate.custom` / `logic[]` / button の `custom`) から、現在文書のフィールド値や他文書 / マスタ View を参照する経路。優先順は **cfGlobal 直読 > cfAPI > restViewJson/restDocJson**。データバインディングは [data-binding.md](/docs/data-binding.md)、JS 拡張点自体は [scripting.md](/docs/scripting.md)、参照モード disabled は [layout-styling.md](/docs/layout-styling.md)。

## 1. cfGlobal 直読 (最優先)

フォーム内 JS (`customConditional` / `calculateValue` / `customDefaultValue` / `validate.custom` / `logic[]`) で **「現在文書の Notes フィールド値」を参照したい**ケースは多い。アプローチは 2 つあり、**必ず以下の優先順で選ぶ**:

| 優先 | 手段 | REST 呼び出し | 適用条件 |
|---|---|---|---|
| **1** | `cfGlobal.<field>` を直読 | **なし** (サーバー側で事前ロード済) | プリロード対象フィールドの場合 |
| 2 | `cfAPI.getDb().getCurrentDoc().getItemValueString("FieldName")` | あり | プリロード対象外フィールドで、かつ `isNewNote` ガード付き |

### 1.1 `cfGlobal` にプリロードされる標準フィールド

サーバー側のページレンダー時に `cfGlobal` がクライアントへ JSON で渡される。代表的なメンバ:

| プロパティ | 由来 / 用途 | 新規作成時 (`isNewNote=true`) |
|---|---|---|
| `cfGlobal.docid` | 現在文書の UNID (新規時は**クライアント側仮 UNID** で実体なし) | クライアント仮 UNID |
| `cfGlobal.isNewNote` | 新規作成モードか (boolean) | `true` |
| `cfGlobal.editable` | 編集可否 | 文書状態と ACL による |
| `cfGlobal.statusNo` | `StatusNo` (ワークフロー状態) | `""` |
| `cfGlobal.applicationNo` | `ApplicationNo` (申請番号) | `""` |
| `cfGlobal.currentUserName` | ログインユーザー Notes 名 (Canonical) | 常に値あり |
| `cfGlobal.currentUserKanjiName` | ログインユーザー漢字表示名 | 常に値あり |
| `cfGlobal.companyName` / `companyCode` | 所属会社 | 値あり / `""` |
| `cfGlobal.sectionName` / `sectionCode` | 所属セクション | 値あり / `""` |
| `cfGlobal.postName` / `postCode` | 役職 | 値あり / `""` |
| `cfGlobal.unitName` | ユニット名 | `""` |
| `cfGlobal.lang` | 言語コード | `"ja"` 等 |
| `cfGlobal.acl` | ACL 情報 `{accessPrivileges, accessLevel, roles}` | 常に値あり |
| `cfGlobal.storageUnid` | ファイル添付ストレージ文書 ID (file 型用、UNID 形式ではなくタイムスタンプ系 ID) | 常に値あり |
| `cfGlobal.batchId` | バッチ ID (ファイルアップロード等) | 常に値あり |
| `cfGlobal.webFormRevisionNo` | ロード中のフォームリビジョン番号 | 常に値あり |
| `cfGlobal.masterDb` | masterdb 接続情報 `{dbPath, dbServer}` | 常に値あり |
| `cfGlobal.cacheExpiration` | cfAPI sessionStorage キャッシュ TTL (分) | 常に値あり |
| `cfGlobal.enableAutoSave` / `autoSaveIntervalMs` | オートセーブ設定 | 常に値あり |
| `cfGlobal.webFormErrorDisplayMode` | 入力エラーの表示方法 (`"0"` ハイライトのみ / `"1"` + フォーム上部に一覧。アプリ設定) | 常に値あり |
| `cfGlobal.saveClientEnvFlg` / `clientHintsPresent` | 保存時クライアント環境の記録 ON/OFF と、このセッションでヒント送信済みか (`"1"`/`"0"`) | 常に値あり |
| `cfGlobal.webFormAiSystemPrompt` | フォーム毎の AI システムプロンプト (文字列。未設定なら空) | 常に値あり |

`cfGlobal` はデータ専用オブジェクト。フォームのライフサイクル API は **`cfAPI.form`** (`onOpen` / `onNew` / `migrate` / `onReady`、[scripting.md](/docs/scripting.md) §9) に載っている。ビルダープレビュー (`formDesigner` / WEBフォーム管理のプレビュー) では `cfGlobal` が未定義なので、直読するスクリプトは `typeof cfGlobal !== 'undefined'` を先に確認する。

**判定方針**: 参照したい Notes フィールドがこの表に対応物があるなら、迷わず `cfGlobal.<member>` を使う。REST 呼び出しが完全に消え、`isNewNote` ガードも不要になる。

`file` 型コンポーネントは `cfGlobal.storageUnid` 経由でストレージ DB に直接書き込むため、`isNewNote=true` 文脈でも動作する (`cfGlobal.docid` 仮値問題の影響を受けない)。

### 1.2 推奨パターン (StatusNo を例に)

❌ 悪い例 (cfAPI 経由 — 新規作成時に REST が走り、サーバーで `Invalid universal id` 例外):
```javascript
"customConditional": "var statusNo = cfAPI.getDb().getCurrentDoc().getItemValueString(\"StatusNo\");\nshow = (statusNo === \"0\" || statusNo === \"\")"
```

⚠️ 中途半端な例 (新規時のみガード — 編集時は依然 REST が走る):
```javascript
"customConditional": "var statusNo = (cfGlobal.isNewNote ? \"0\" : cfAPI.getDb().getCurrentDoc().getItemValueString(\"StatusNo\"));\nshow = (statusNo === \"0\" || statusNo === \"\")"
```

✅ 最良 (cfGlobal 直読 — 新規・編集どちらも REST ゼロ):
```javascript
"customConditional": "var statusNo = cfGlobal.statusNo;\nshow = (statusNo === \"0\" || statusNo === \"\")"
```

### 1.3 cfGlobal にない値を読む — このときだけ cfAPI + `isNewNote` ガード

カスタマー独自の Notes フィールド (例: `MyCustomField`) は `cfGlobal` にプリロードされないので、cfAPI 経由で取りに行く必要がある。このときは **`cfGlobal.isNewNote === true` のとき API を呼ばないようにガード**する。

```javascript
"customConditional":
  "var v = (cfGlobal.isNewNote ? \"\" : cfAPI.getDb().getCurrentDoc().getItemValueString(\"MyCustomField\"));\n" +
  "show = (v === \"someValue\");"
```

**理由**: `cfAPI.getDb().getCurrentDoc()` は内部で `cfGlobal.docid` を docId として使う。`isNewNote === true` のとき `cfGlobal.docid` は **クライアントコンテキスト上の一時的 UNID で、サーバー側にまだシリアライズされていない**。そのため:

1. AJAX `?action=getItemValueString&docId=<仮UNID>&fieldName=<...>` がサーバー側 API へ送られる
2. `db.getDocumentByUNID(unid)` が `NotesException: Invalid universal id` を投げる
3. catch で握り潰され、ブラウザには空文字が返却される (= Domino コンソールに毎レンダー大量のスタックトレース)
4. クライアントロジックは「サーバーエラーで空文字が返る」という偶然に依存した挙動になる

Form.io はレンダーごとに `customConditional` を全件評価するので、新規フォームを開くだけで該当式の数 × N 回エラーが出る。

### 1.4 ガードを忘れがちな Form.io 拡張ポイント

`cfGlobal.docid` 由来の REST が走る可能性がある式は全て対象。cfGlobal にない値を扱う場合は **必ず isNewNote ガード**:

- `customConditional` (show/hide 判定 — レンダーごとに毎回評価)
- `customDefaultValue`
- `calculateValue`
- `validate.custom`
- `logic[].actions[].action === "custom"` 配下の JS
- 各コンポーネントの `customClass` 計算式
- カスタム input/event ハンドラ

### 1.5 既存フォームの棚卸し

既存の webform JSON 内で `cfAPI.getDb().getCurrentDoc().getItemValueString` を検索する (静的チェックのルール CF003 も検出する)。ヒットした箇所について:

1. **そのフィールドが §1.1 の cfGlobal リストにあるか** をまず確認 → あれば `cfGlobal.<member>` への置換が最良
2. なければ `cfGlobal.isNewNote ? <既定値> : cfAPI...` のガードに置換

### 1.6 cfGlobal を増やしたい場合

「もっと多くのフィールドを cfGlobal にプリロードしてほしい」というニーズが出たら、それは **コア側で cfGlobal を構築している処理の改修**になる (webform 単独では対応できない)。

ピンポイントで cfGlobal を肥大化させず済ます工夫:
- cfAPI を経由する場合でも `cfGlobal.cacheExpiration` 分だけ sessionStorage にキャッシュされる (cfAPI 標準動作) ので、同じフィールドを複数 conditional で連打しても初回 REST 以降はキャッシュヒット
- 同じフィールドを複数 customConditional で使うなら、最上位の `customDefaultValue` で 1 回だけ取って隠しフィールドに入れ、各 conditional は `data.<隠しキー>` を参照する設計にする (REST は 1 回のみ)

## 2. myFormio.syncFields() — 入力値を Notes 文書へ反映して承認者を再計算する

Form.io の入力値は hidden の `formIOFieldJson` にしか無く、**保存するまで Notes アイテム (`<prefix>key`) にならない**。
そのため動的承認者が入力値に依存する構成 (ワークフロー JSON の `docField` / `formula` / `masterLookup.keyExpr` が項目を読む) では、
新規作成直後の承認フロー欄が空になる。

コアが `myFormio.syncFields()` を提供しているので、button コンポーネント (`action: "custom"`) から 1 行で解決できる。

```js
myFormio.syncFields();
```

内部動作: `this.data = this.form.data` → `serializeData()` (hidden へ) → `XSP.partialRefreshPost(target, {params:{syncFields:'1'}})`。
サーバー側は編集モードのときだけ 入力値の文書反映 + ワークフロー再計算を行い、同じ応答で承認フロー欄を返す。

| opts | 既定 |
|---|---|
| `refreshId` | 更新対象パネルの clientId。省略時 `[id$='approvalFlowPanel']` → 無ければ `[id$='mainPanel']` |
| `params` | 追加パラメータ (`syncFields:'1'` に合流) |
| `onComplete` | 部分更新の完了コールバック |

**使うときの注意**

- `serializeData()` は内部で呼ばれるので自前で書かなくてよい (`form.on('change')` の遅延対策も込み)
- 下書き限定にしたいなら呼び出し側でガードする: `cfGlobal.editable && !cfGlobal.applicationNo && (cfGlobal.statusNo === '' || cfGlobal.statusNo === '0')`
- 値が変わったときだけ投げる (`window.__lastXxx` で前回値を持つ)。毎 change で投げるとサーバー往復が増える
- 反映も再計算も**メモリ上だけ**。保存しなければ消える
- 参照モードでは何も起きない
- **ボタン（部分更新 = POST）専用**。URL に `?syncFields=1` と直接書いても発火しない (GET では hidden が空のことがあり、URL パラメータで入れた多値項目を空で潰すためコア側でガードしている)
- **申請ボタンでは再計算が走らない**。新規文書で入力値依存の承認者を使うなら、申請前に `syncFields()` を通すか URL パラメータで値を渡すこと
- 承認者に使う項目は**文字列 (ユーザー名) で保存**されるように作る (`select` は `dataType:"string"` + `valueProperty`。オブジェクトのままだと JSON 文字列 1 個になる → [[data-binding]] Red Flag #7/#9)

## 3. restViewJson.xsp / restDocJson.xsp (マスタ View 検索、別文書全フィールド)

consentFlow 系の各 NSF (コア / カスタマー両方) には、Form.io から Notes データを取りに行くための汎用 REST 端点が 2 つ用意されている。複雑なフォーム (連動入力、マスタ検索、重複チェック等) では `cfAPI.getItemValueString` で済まないケースが多く、これらを使うのが定石。

実体は XPages Extension Library の `xe:viewJsonService` / `xe:documentJsonService` の薄いラッパ。

### 3.1 用途比較

| 端点 | 用途 | レスポンス |
|---|---|---|
| `restViewJson.xsp/json` | Notes ビューから複数文書を取得 (キー絞り込み / カテゴリドリル / ソート / ページング) | 配列 (各要素 = 1 view entry の列値オブジェクト) |
| `restDocJson.xsp/json` | UNID 指定で**単一文書の全フィールド**を取得 (View に出していない値も含む) | オブジェクト (各キー = Notes Item 名) |

view で必要列が揃うなら **restViewJson の方が軽量**。RichText や View に出していない補助フィールドを読みたいときだけ `restDocJson` を使う。

### 3.2 URL とクエリパラメータ

**共通パラメータ:**

| パラメータ | 説明 |
|---|---|
| `srv=<server>` | サーバー名。`masterdb` を渡すと wfConfig の masterdb サーバー設定に解決 |
| `db=<dbpath>` | DB パス。`masterdb` を渡すと wfConfig masterdb の data DB パスに解決。空文字でも masterdb 扱い (restDocJson のみ) |

`masterdb` ショートカットを使うと wfConfig 設定どおりに解決されるので **NSF パス変更に強い**。別 DB を見たいときは `db=customer/data/myMaster.nsf` のように明示パスを書く (この場合 `srv` は通常 `masterdb` か実サーバー名)。管理者が「外部データベースマスタ」に登録したプレースホルダー名 (例 `acldb`) も `srv=acldb&db=acldb` と裸名で使える (未登録の値は実パス扱い)。同じ登録名をワークフロー JSON の承認者解決 (`approverByResolver` の `masterLookup.db`) で使うときは `"path": "$acldb"` と `$` 付きになる点だけ違う。

**restViewJson 固有 (`./restViewJson.xsp/json?...`):**

| パラメータ | 説明 |
|---|---|
| `vw=<viewName>` | 取得対象の View 名 (Notes 上のエイリアス可) |
| `keys=<key>` | ネイティブ keys 絞り込み。**単一値の前方一致専用**。カンマ区切りを渡しても多列キーに分割されず「1 個のキー文字列」として扱われる (= 多列 lookup には使えない → `keysEx`) |
| `keysEx=<v1,v2,...>` | **多列キー完全一致** 絞り込み (consentFlow 拡張)。カンマ区切りの各値を 1 列ずつのキーにした多列キーを構成し全列完全一致で絞る。各値は UTF-8 URL デコードされるので **`encodeURIComponent` 必須**。`keys` 併記時は `keys` が優先され `keysEx` は無視される (§3.2.1) |
| `keysexactmatch=true` | `keys` の前方一致を **完全一致** に切替。**名前は全部小文字・値は文字列 `true` のみ有効** — `keysExactMatch=1` / `keysExactMatch=true` (camel) は **無視されて前方一致のまま**になる (実機確認済)。`keysEx` は常に完全一致なので指定不要 |
| `count=<n>` | 取得件数の上限。**未指定は 10 件で打ち切り** |
| `start=<n>` | 開始エントリ位置 (0 始まり、ページング用)。`page=<p>&ps=<n>` (start = p×n) も等価 |
| `search=<query>` | 全文検索クエリ (Notes FT 構文そのまま: `[Field] CONTAINS 語` / `FIELD Field CONTAINS 語` / `AND` `OR` `NOT` / 括弧 / `"連語"` / 英数語は `語*` `*語*`)。複合条件は 1 つの `search=` に書く: `FIELD supplierName CONTAINS "aaa" AND FIELD supplierNo CONTAINS "hogehoge"`、同一項目の複数語は **`[F] CONTAINS (a OR b)` と括弧必須** (無いと `OR b` が文書全体に掛かる)。**`[F]` は文書アイテム名** — 返却キー / `sortcolumn` に使うビュー列プログラム名 (列式由来の `xxxEx` 等) とは別名前空間で、列名を書くと常に不成立 (AND で 0 件・OR だと気付かない)。アイテム名は restDocJson で確認。実機確認: `[ApplicantKanji] CONTAINS "レヴィ" AND [ApplicationNo] CONTAINS "14"` でヒット (AND 複合 OK・ハイフン区切り `14-00102` は `14` / `00102` の 2 語に索引されるので `"14"` で当たる)。検索対象は**文書アイテム** (ビューに無い項目でも可)。**日本語はワイルドカード不要で部分一致し、`米原*` / `*米原*` のように付けると 0 件になる**。逆に**英数字は単語単位一致** (`Kir` では `Kiritani` に当たらない) なので部分一致には `Kir*` のようにワイルドカードを付ける (実機確認済)。**DB に FT 索引必須** (未索引は `Database is not full text indexed.` エラー)。**`search` 指定時は `keys` / `category` は無視される** (排他)。結果には `@score` メタ列が付く。連続呼び出しで `The full text index for this database is in use` (HTTP 400) が一時的に返ることがある → 1 回リトライ |
| `sortcolumn=<programmaticName>` / `sortColumn=` | クリックソート可能な列のプログラム名で再ソート (両表記可)。**「クリックでソート」(その方向) が無い列を指定してもエラーにならず既定順のまま返る** (ExtLib が `resortView` の例外を握り潰す) ので、結果の並びを見て検知する。`search` 併用時は `FTSearchSorted` になり、こちらは非対応列で HTTP 400 |
| `sortorder=descending` / `sortOrder=` | 並び順 (`descending` 以外は昇順扱い) |
| `categoryFilter=<encoded>` / `category=` | カテゴリ列で部分ツリーを絞り込み (バックスラッシュ区切階層も OK)。**`encodeURIComponent` 必須** (XSP 側で URL-decode 実行) |

> **パラメータ名の規則**: ExtLib ネイティブ名 (`start count page ps search searchmaxdocs sortcolumn sortorder category keys keysexactmatch startkeys parentid expandlevel compact`) は **すべて小文字・大小区別あり**で、URL に書けば XSP 側の既定値より優先される。camel 表記で効くのは consentFlow 側が別名バインディングを用意している `sortColumn` `sortOrder` `categoryFilter` `keysEx` だけ。迷ったら小文字ネイティブ名を使う。
> 絞り込みの適用順は `search` > `keys` > `parentid` > `category` の排他 (最初に指定があるものだけが効く)。

#### 3.2.1 多列キー絞り込み (`keysEx`)

ソート列を複数持つ View を「全列とも一致する行」だけに絞りたいときは `keysEx` を使う。Extension Library のネイティブ `keys` クエリパラメータはカンマ区切り文字列を **1 個のキー** として扱うため、多列キーを渡せない。そこで `restViewJson.xsp` 側がカンマ分割 → 多列キー化 → 全列完全一致で絞り込む。

```
./restViewJson.xsp/json?srv=masterdb&db=masterdb
  &vw=vwOrdersByFormId&count=9999
  &keysEx=1001,planA,東京
```

- 値はカンマ区切り。各値はサーバー側で 1 回 UTF-8 URL デコードされる → **クライアントで `encodeURIComponent` を 1 回だけ**適用 (マルチバイト「東京」も同様)
- 全列完全一致。**View のソート列順とカンマ区切りの並び順を一致させる**
- カンマ直後に空白を入れない (`a, b` の先頭空白はトリムされず完全一致が外れる)
- キー値自体にカンマを含む場合は分割できない (既知の制約)
- **`keys` 優先 / 後方互換**: `keys` と `keysEx` を両方渡すとネイティブ `keys` 処理が効き `keysEx` は無視される。`keys` のみ・`keysEx` のみ・どちらも無し (全件) はいずれも従来どおり

**restDocJson 固有 (`./restDocJson.xsp/json?...`):**

| パラメータ | 説明 |
|---|---|
| `unid=<UNID>` | 取得対象文書の UNID (32 桁 hex) |

### 3.3 レスポンス形状

**restViewJson 例** (`vw=vYourMaster&keys=ABC001&keysexactmatch=true`):
```json
[
  {
    "@entryid": "1-DEADBEEF...",
    "@unid": "DEADBEEF...",
    "@noteid": "1A2",
    "@position": "1.1",
    "@read": true,
    "@siblings": 1,
    "@form": "MasterForm",
    "code": "ABC001",
    "name": "サンプル名称",
    "validFrom": "2026-01-01T00:00:00+09:00",
    "categoryPath": "Region1\\Office1"
  }
]
```
- 各エントリは **View 列のプログラム名がキー**になる (Notes 列定義で「プログラム名」を指定していない場合は自動採番のような名前になるので、**呼び出し側で予測できる名前を必ず指定**する)
- 値型は列の式型に従う (String / Number / DateTime / 配列)
- `@entryid` / `@unid` / `@noteid` / `@position` / `@read` / `@siblings` / `@form` は Extension Library のメタデータ列 (`search` 時は `@score`、カテゴリ/応答階層ビューでは `@category` `@response` `@children` `@descendants` `@indent` も付く)
- **View の全列が出る (「列を非表示」にした列も含む)**。ビューに無い Notes アイテムは出ない → 取りたい項目は **非表示列として View に追加**するか、`@unid` で `restDocJson` を引く。列の値が空のエントリではそのキー自体が省略されることがあるので、呼び出し側は `row.key || ''` で読む

**restDocJson 例** (`unid=DEADBEEF...`):
```json
{
  "@unid": "DEADBEEF...",
  "@noteid": "1A2",
  "Form": "FormSomething",
  "code": "ABC001",
  "name": "サンプル名称",
  "multiValueField": ["a", "b", "c"],
  "richTextBody": "<html>...</html>"
}
```
- 全 Notes Item が出力される (`defaultItems="true"`)。不要フィールドが多い場合は View 経由 (restViewJson) の方が転送量が少ない
- RichText は HTML 変換後の文字列で来る
- DateTime は ISO 8601 (タイムゾーン付き)
- 複数値 Item は配列

### 3.4 呼び出しコードの定石 (Button の `custom` 内で `$.ajax`)

Form.io の `button` 型コンポーネントの `custom` プロパティに JS を書く。consentFlow フォーム上では **`XSP.allowSubmit()` の呼び出しが必須** (XPages 側のサブミットガードを通すため)。

```javascript
// Button 設定: { "type": "button", "key": "btnLookup", "custom": "..." }

XSP.allowSubmit();  // XPages サブミットガード解除 — 必須

var _doLookup = function(searchKey, rowIndex) {
  // 応答前に従属フィールドをクリア (古い値が残らないように)
  form.getComponent('codeField').setValue('');
  form.getComponent('nameField').setValue('');
  form.getComponent('statusField').setValue('');

  $.ajax({
    async: true,
    crossDomain: true,
    url: './restViewJson.xsp/json?srv=masterdb&db=masterdb'
       + '&vw=vYourMaster'
       + '&keys=' + encodeURIComponent(searchKey)
       + '&keysexactmatch=true&count=1',   // ← 小文字 + true 固定。camel/=1 は前方一致のまま
    method: 'GET',
    timeout: 30000     // 重い View で UI フリーズしないよう設定推奨
  }).done(function(response) {
    if (!response || response.length === 0) {
      form.getComponent('statusField').setValue('not_found');
      return;
    }
    var rec = response[0];
    form.getComponent('codeField').setValue(rec.code);
    form.getComponent('nameField').setValue(rec.name);
    form.getComponent('statusField').setValue('ok');
  }).fail(function(xhr, status, err) {
    form.getComponent('statusField').setValue('error');
    console.warn('lookup failed:', status, err);
  });
};

_doLookup(data.searchInput, /* rowIndex */ undefined);
```

restDocJson も呼び方は同じ:

```javascript
$.ajax({
  url: './restDocJson.xsp/json?srv=masterdb&db=masterdb&unid=' + encodeURIComponent(selectedUnid),
  method: 'GET'
}).done(function(doc) {
  if (!doc || !doc['@unid']) {
    form.getComponent('statusField').setValue('not_found');
    return;
  }
  form.getComponent('detailField').setValue(doc.someNotesItem || '');
});
```

### 3.5 EditGrid / DataGrid 内コンポーネントへのアクセス

行内コンポーネントは **配列形式 path** で指定:

```javascript
// editGrid の rowIndex 行の "code" コンポーネントを取得
form.getComponent(['editGridKey', rowIndex, 'code']).setValue(rec.code);

// グリッド全体を取得
var grid = form.getComponent('editGridKey');
var rowCount = grid.getValue().length;
```

`rowIndex` は **呼び出し側の文脈で持っておく** 必要がある。Form.io はボタンクリック時に `currentRowIndex` のような値を直接提供しないので、典型的にはボタン親の logic event や `editGrid.editRow` のフックから受け取る。

### 3.6 落とし穴と注意点

**View 列のプログラム名を必ず指定する**
- Domino Designer の View 列プロパティで「プログラム名」を未設定にすると、JSON キーが `$11` のような自動採番になり呼び出し側コードが壊れる
- 推奨: Notes Item 名と同名 (例: NotesItem `code` を出す列のプログラム名も `code`) にする

**categoryFilter は二重エンコードに注意**
- サーバー側 (`restViewJson.xsp`) で UTF-8 URL デコードを 1 回実行している
- ブラウザ → サーバー間で URL は 1 回エンコードされる
- 結論: クライアントで `encodeURIComponent(value)` を **1 回だけ** 適用する。バックスラッシュ階層 (`Region1\\Office1`) も含めて

**キャッシュは効かない**
- restViewJson/restDocJson はサーバー側でキャッシュしない (`Cache-Control` も付与しない)
- 同じ View を連打する画面では、jQuery 側で `$.ajaxSetup({ cache: true })` を使うか、独自の応答キャッシュ (Map / sessionStorage) を一段挟む
- それでも Notes View インデックス自体は Domino 側でキャッシュされているので、初回以外は通常ミリ秒オーダ

**timeout を必ず設定**
- jQuery の `$.ajax` はデフォルト `timeout: 0` (無限待ち)
- 大量レコード View や FT 検索で **応答が止まると UI が完全に固まる**
- 30000 ms (30 秒) 程度を上限に

**Reader フィールド / ACL は通常どおり評価される**
- Web ユーザーが見えない文書は View からも除外される。**意図的に管理者表示で見せたい場合は `sessionAsSignerWithFullAccess` でラップした独自 XSP REST を別途用意** (※ コア側対応が必要)
- 匿名アクセスは ACL で制限可能

**応答を待つ間の clear-then-set 必須**
- 非同期応答の前に従属フィールドの値をクリアしないと、新キーで検索したのに古い値が見えるという UX バグになる
- 上の §3.4 のコード参照

**ユーザー入力は `encodeURIComponent` でエスケープ**
- `keys=` / `categoryFilter=` にユーザー入力を直接連結すると、`&` や `=` が含まれた瞬間に URL パースが壊れる
- Notes の氏名検索など `CN=.../OU=.../O=...` を渡すケースでも全文を encodeURIComponent する

**1 ボタンから複数 ajax は順序保証なし**
- 重複チェック + データ取得など 2 本同時に投げると応答順序は不定
- 順序依存があるなら `Promise` で連鎖、または `.done` 内でネストする

**`async: false` は使わない**
- Form.io / ブラウザ UI が固まる
- どうしても順序依存があるなら Promise 化する

**カテゴリ行 (`@category: true`) が混入する**
- `restViewJson.xsp` のレスポンスは、対象 View にカテゴリ列があると **カテゴリ行も配列に含まれる** (Notes 標準動作、`categoryFilter` を指定した場合のみ除外)
- カテゴリ行は表示用の見出しなので、業務上は値を持たない (例: `{"@category": true, "bumon1": "営業部"}` だけが返ってきて `shimei` が空)
- **Select の dropdown には表示される** ので、ユーザーがうっかり選んだ場合に死なないよう、ボタン側の処理で `if (picked['@category'] === true) { alert(...); return; }` で弾く
- ガード対象は `@category: true` の有無、または必須プロパティ (例: `picked.shimei`) の空判定のどちらかで OK
- 抜本的に解決したいなら master 側で「カテゴリレス view」を別途用意するか、`categoryFilter` を空文字以外で渡す (Notes は `categoryFilter` 指定時にカテゴリ行を返さない)

### 3.7 cfAPI との使い分け

| 用途 | 推奨端点 |
|---|---|
| 単一フィールド値を 1 つだけ取りたい (現在文書から) | `cfAPI.getDb().getCurrentDoc().getItemValueString(...)` (§ 1 isNewNote ガード必須) |
| マスタ View の特定エントリを引きたい | **`restViewJson.xsp`** + `keys=` + `keysexactmatch=true` (小文字必須) |
| マスタ View を**多列キー完全一致**で引きたい | **`restViewJson.xsp`** + `keysEx=v1,v2,...` (§3.2.1) |
| マスタ View からカテゴリで複数取りたい | **`restViewJson.xsp`** + `categoryFilter=` |
| FT 検索 | **`restViewJson.xsp`** + `search=` |
| 別文書の全フィールドを読みたい (UNID 既知) | **`restDocJson.xsp`** + `unid=` |
| 別文書の 1 フィールドだけ | `cfAPI.getDb().getDocByUnid(unid).getItemValueString(...)` でも可 |

cfAPI 経由は **クライアント sessionStorage にキャッシュされる** が、`docId` が valid な UNID 前提。view 系のキー検索は cfAPI には無いので restViewJson 一択。

### 3.8 Select コンポーネントで「行オブジェクト全体」を保持したいときの `dataType` (落とし穴)

`restViewJson.xsp` を `dataSrc: "url"` に指定した Select で、選択した **行オブジェクト全体** (`{shimei, bumon1, @unid, ...}`) を後続コンポーネント (ボタン JS / 別フィールド) から使いたい — このときの設定は **`dataType` の選び方で結果が劇的に変わる**。

| `dataType` | `valueProperty` | `data.<selectKey>` に保存される値 | 使えるか |
|---|---|---|---|
| **`"object"`** | `""` | **行オブジェクト全体** (`{shimei: "...", bumon1: "...", "@unid": "...", ...}`) | ✅ **これが正解** |
| `""` (Auto) | `""` | 環境依存 (Form.io バージョン差で挙動が変わる)。空文字 / 行オブジェクト どちらに転ぶか保証なし | △ 非推奨 |
| `"string"` | `""` | **文字列 `"[object Object]"`** (`String(row)` の結果。Form.io 内部で `value = String(item)` される) | ❌ 即死 |
| `"string"` | `"someProp"` | `item["someProp"]` の文字列値のみ | ⚠️ 単一カラムしか取れない |

**鉄則**: 行オブジェクトを `data.<selectKey>` で受け取って後続 JS から `obj.shimei` 等で参照したい場合、必ず `"dataType": "object"` を明示する。Form.io Builder の Data タブで「Object」を選ぶと JSON に `"dataType": "object"` が書き込まれる。

**Notes 側の保存形** ([data-binding.md](/docs/data-binding.md) §1): `dataType:"object"` で保持した行オブジェクトは **`<prefix><selectKey>` に行 JSON 文字列 1 アイテム** として保存される (既定のアプリ設定では `persistent:false` でも保存される。設定「persistent:false / Client-only の項目を保存しない」が はい の環境では保存されない — [data-binding.md](/docs/data-binding.md) §1)。子列 (`shimei` 等) は個別アイテムにならないので、Notes ビューや SQL 連携で使う列は `calculateValue` でスカラーの別コンポーネントに転記する。旧コアは行の列名で `CF_shimei` / `CF_UserName` … に平坦化しており、ピッカーが複数あると後勝ちで衝突していた。

**`dataType: "string"` の罠**

Builder UI で「Data Type」のデフォルトが `"string"` のフォームが多い。これを使ったまま `valueProperty: ""` (= 行オブジェクト保存を期待) にすると、**JS 側で `String(row)` が呼ばれて文字列 `"[object Object]"` が保存される** という挙動になる。alert で「有資格者を選択してください」とエラーになる典型ケース。

**ボタンの `custom` JS から行オブジェクトを使う**

`dataType: "object"` 設定済みの前提で、ボタン JS から行オブジェクトを取得する 3 パターン:

```javascript
// パターン 1: 親フォーム全体から (panel/columns/直下、いずれも同じ)
var picked = data.pickerSelect_p_buchou;

// パターン 2: datagrid 行内
var picked = row.pickerSelect_p_buchou;

// パターン 3: form.getComponent 経由 (Component が見つかればどこからでも)
var sel = form.getComponent('pickerSelect_p_buchou');
var picked = sel ? sel.dataValue : null;
```

パターン 1 は最も簡潔だが、**親が `type:"container"` + `tree:true` のときは `data.<container>.<selectKey>` にネストされる**ので破綻する (layout-styling.md の panel vs container 解説参照)。consentFlow で container を避ければパターン 1 が一番素直。

### 3.9 Select の `idPath` — 行の一意性を Form.io に教える

`dataType: "object"` + `restViewJson` の組み合わせでは、`idPath` に **行の一意キー** を指定するのがほぼ必須:

```jsonc
"dataType": "object",
"valueProperty": "",
"idPath": "@unid"
```

`idPath` は **Form.io がオプション同士を区別するためのキー**で、Choices.js のレンダリング (重複行排除・既選択ハイライト・refresh 時の選択保持) に使われる。`restViewJson` のレスポンスは各行に `@unid` を含むので、これを指定するのが定石。指定しないと「同じ行が複数表示される」「選択後に別の行が選ばれた状態になる」等の異常が出ることがある。

実例: 担当者ピッカーの `pickerSelect_p_buchou` ほか 15 ロール分 (実運用フォーム)。`idPath` は Form.io 内部の同一性判定用で、Notes 側のアイテム名・保存形 (§3.8 末尾) には影響しない。

### 3.10 lazyLoad な URL Select の「空表示」は応答待ち、「生コード表示」は失敗 — 見分け方と監視パターン

`dataSrc:"url"` + `lazyLoad:true` + `valueProperty` の Select は、既存文書を開くと **保存値のラベル解決のためにマスタ全件を REST で取りに行き、応答が返るまで保存値を描画しない** (formio 4.14.10 `Select.setValue` は `loading` 中に早期 return する。タイムアウト無し)。症状の見分け方:

| 画面の見え方 | 内部状態 | 意味 |
|---|---|---|
| チップが 1 枚も無い (完全な空) | `comp.loading === true`、fetch 保留 | **REST 応答が返ってきていない** (サーバー遅延 / セッション / 経路)。保存値 `dataValue` は生きていて `getValue()` もそれを返すので、保存しても消えない |
| 生コード (`078531`) のチップ | `.catch` → `setItems([])` | 読込 **失敗** (非 2xx / ネットワーク / ログイン画面などの非 JSON 200)。`componentError` が出る (非 JSON 200 は出ない — `Formio.cache` に HTML が入る) |
| 名称のチップ | 正常 | — |

付随する罠:
- **同一 URL の Select が 2 つあると全件取得が 2 本並列で飛ぶ** (`Formio.cache` は結果キャッシュで、同時 GET を重複排除しない)。`refreshOn` 付き Select は init + refresh で 2 本
- 失敗後は `networkError=true` → `refreshOn` の無い Select は `shouldLoad=false` のまま **二度と読み込まない**
- `limit: 99999` + `count={{ limit }}` の全件 Select は 1 文書開くごとにマスタ全件 (非表示列・多値列込み) を転送する。ビューにメンバー一覧のような多値列があると MB 級

**監視・制御パターン** (フォーム JSON だけで対処。実装例: マスタ参照フォームの監視ブロック):
- `type:"hidden"` + `calculateValue` の init hook ([scripting.md](/docs/scripting.md) §9) から `instance.root.getComponent(key)` で Select 実体を掴む
- **初回パス** (submission 適用前に走る) で `comp.lazyLoadInit = true` + `comp.itemsLoadedResolve()` → submission 適用時に保存コードがそのままチップ表示される (空にならない)。既に `comp.loading || comp.lazyLoadInit` なら submission が先に入るコアなので監視だけに切替
- `root.submissionReady.then(...)` 後に、値のある Select だけ `comp.triggerUpdate(null, true)` (直後の `comp.itemsLoaded` を捕まえて完了を追う)。**同一 URL の Select は 1 本目の完了後に起動すると `Formio.cache` ヒットでネットワーク 0**
- 再取得: `if (!refreshOn && !refreshOnBlur) comp.networkError = false; comp.triggerUpdate(null, true)`。非 JSON 200 の後だけ `Formio.clearCache()`
- 診断: `Formio.registerPlugin({ priority: 90, requestResponse: function (resp, F, req) { /* resp.status / headers.get('content-type') / resp.clone().text() */ return resp; } }, 'name')` で HTTP ステータスと本文種別を記録 (ログイン HTML の 200 を検知できる唯一のフック)。`root.on('componentError', e => e.component.key / e.message)` で失敗文言。軽量プローブは同ビュー `count=1` を `$.ajax({timeout})` で投げてサーバーが応答しているかだけ見る
- 利用者向けバナーは `htmlelement` を置き場にして `[ref="html"]` の innerHTML を書く (参照モードの `form.redraw()` で消えるのでポーリングで再描画)

## 4. 外部 REST API (API キー方式) は webform から直接呼ばず、システム連携 (REST API) 文書 + `restAPI.xsp?apiid=` 経由で呼ぶ

外部 API のキーを webform JSON (hidden の `defaultValue` 等) に置くと、ブラウザから丸見えになり、文書ごとに `<prefix>key` として保存され、キー差し替えにフォームの再配備が要る。コアには **API キーをサーバー側で付与して GET を中継する仕組み**が既にある:

1. 管理画面「システム連携設定」で `Integration Type = RESTAPI` の文書を作る。**API ID** (有効文書間で一意) / **API URL** (ベース URL。パスに `{{$キー}}` も使える) / **API Method = GET** / **API request json** に既定パラメータとしてキーを書く (例 `{"key":"<API キー>"}`)。認証ヘッダ方式なら接続設定 (OAuth2 / Bearer / Basic) を選ぶ
2. webform からは同一 NSF 相対で `restAPI.xsp?apiid=<API ID>&<外部 API のパラメータ…>` を `fetch` する。ブラウザから渡したパラメータは request json の既定値に**上書き・追加**されてクエリ文字列になり、応答 JSON がそのまま返る (`apiid` / `apiUnid` / `limit` / `skip` は外部へ転送されない)

```js
// Select の data.custom や Button の custom から
const url = "restAPI.xsp?apiid=myStationApi&name=" + encodeURIComponent(q) + "&type=train";
return fetch(url, { credentials: "same-origin" })
  .then(res => res.json())          // Content-Type は text/html だが本文は JSON。fetch の json() は Content-Type を見ない
  .then(json => json?.ResultSet?.Point || [])
  .catch(() => []);
```

落とし穴:
- 応答が JSON **オブジェクト**なら `_httpStatus` が 1 キー足される。配列ならそのまま。空 / 非 JSON は `{"execError":…}` になる
- 外部 API の HTTP 4xx/5xx は既定では本文がそのまま返る (「Treat HTTP 4xx/5xx as failure」= いいえ)。API 固有のエラー (例: `ResultSet.Error`) はフォーム側で見て利用者に出す
- `restAPI.xsp` は Domino セッション認証。フォーム利用者はログイン済みなので追加設定は不要だが、Bot 実行ログの対象外 (trace なし)
- 旧バージョンの consentFlow で `?apiid=` が使えないときは、連携 URL パネルの「legacy URL」を使う
- 呼ぶ環境ごとにシステム連携文書が要る (WEB DB 側の設定)。フォームの配備手順に「API ID `xxx` の文書を作る」を含める
- 入力が空のときは `fetch` せず `[]` を返す (リクエスト数課金の API では無駄打ちがそのまま費用になる)

## Red Flags

- ❌ 外部 API のキーを webform JSON (hidden の `defaultValue` 等) に直書きする → ブラウザから丸見え・文書ごとに保存・差し替えに再配備。RESTAPI 種別のシステム連携文書にキーを置き `restAPI.xsp?apiid=` 経由で呼ぶ (§4)

- ❌ lazyLoad URL Select が「完全に空」なのを「失敗」や「値が消えた」と判断する → 空 = REST 応答待ち、失敗なら生コードが出る。保存値は生きている (§3.10)
- ❌ 同一 URL (同じビュー全件) の Select を 2 つ並べて放置 → 文書を開くたび全件取得が 2 本並列。init hook で 2 本目を 1 本目の完了後に起動して `Formio.cache` に乗せる (§3.10)
- ❌ `cfAPI.getDb().getCurrentDoc().getItemValueString()` を `cfGlobal.isNewNote` ガードなしで呼ぶ → 新規時に大量の `Invalid universal id` エラー (§1)
- ❌ `cfGlobal.<member>` で取れる値を cfAPI 経由で取る → 不要な REST 呼出。cfGlobal 直読で REST ゼロ (§1.1, §1.2)
- ❌ Select with `restViewJson` + `dataType: "string"` + `valueProperty: ""` で行オブジェクト保存を期待 → `"[object Object]"` 文字列保存。`dataType: "object"` + `idPath: "@unid"` を必ず指定 (§3.8 / §3.9)
- ❌ `$.ajax` で `timeout` 未指定 → 重い View で UI フリーズ。`timeout: 30000` (30 秒) を必ず指定 (§3.6)
- ❌ ユーザー入力を `keys=` に直接連結 → URL パース崩壊。`encodeURIComponent` 必須 (§3.6)
- ❌ カテゴリ列のある View で `@category: true` 行の混入を無視 → 空行が dropdown に出る。ボタン JS でガード (§3.6)
- ❌ `$.ajax` で `async: false` を使う → UI 完全フリーズ。Promise 化で対応 (§3.6)
