@joymerrevent/porters-connect 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,113 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.10.0] - 2026-08-22
9
+
10
+ **partition の束ね方を 1 つに絞り、削除済みレコードを判別できるようにした版**です。
11
+ `PortersClient` から `partition` を外した**破壊的変更**を含みます(移行は下記)。
12
+ 併せて、削除済みを読めるのに**どれが削除済みか分からなかった**穴を塞ぎました。
13
+
14
+ ### Added
15
+
16
+ - **削除フラグ `P_Deleted`**([ADR-0056][adr56])。データ系 5 リソース(Candidate / Job / Client /
17
+ Process / Resume)の公開型に加わり、`itemstate: "all"` の結果から削除済みを**判別できる**ようになりました。
18
+
19
+ ```ts
20
+ const page = await t.candidate.search({ itemstate: "all" });
21
+ const deleted = page.items.filter((c) => c.P_Deleted === "1");
22
+ ```
23
+
24
+ - **値は文字列**の `"0"`(生存)/`"1"`(削除済み)です。`number` でも `boolean` でもありません。
25
+ PORTERS がこの項目に **Data Type を与えていない**(reference の Field Type / Data Type 欄がともに「ー」)ため、
26
+ 変換の基準がありません。勝手に決めればライブラリの発明になるので、**生の値のまま**返します。
27
+ - **`condition` にも `order` にも指定できず、Write もできません**(PORTERS の制約)。
28
+ いずれも型で表現されているので、書くとコンパイルエラーになります。
29
+ - `field` を省略すれば**自動で要求**されます。自分で `field` を渡すときは
30
+ `"Person.P_Deleted"` のように接頭辞付きで明示してください。
31
+ - 削除 API が無い PORTERS では「削除済みを読む」こと自体が運用手段(同期・監査・復元の判断材料)ですが、
32
+ これまでは**読めるのに解釈できない**状態でした。
33
+ - 応答での出現条件と値域は**実機で未確認**です([live-verification][lv] LV-14)。
34
+
35
+ ### Changed
36
+
37
+ - **(破壊的)`PortersClient` から `partition` を外しました**([ADR-0055][adr55])。
38
+ partition(Company DB)スコープのリソースは **`porters.tenant(id)` 経由でのみ**取得します。
39
+ **単一テナントでも同じ形**です。
40
+
41
+ ```diff
42
+ - const porters = new PortersClient({ host, appId, appSecret, partition: 123 });
43
+ - await porters.candidate.search();
44
+ + const porters = new PortersClient({ host, appId, appSecret });
45
+ + const t = porters.tenant(123);
46
+ + await t.candidate.search();
47
+ ```
48
+
49
+ - 影響するのは `candidate` / `job` / `client` / `process` / `resume` / `attachment` /
50
+ `user` / `field` / `option` の 9 アクセサ。`t` は以降クライアントのように使えるので、
51
+ **呼び出し側の書き味は変わりません**。
52
+ - **App レベルのものは `porters` 側に残ります**(いずれも `partition` を取りません)—
53
+ `porters.auth.*`(OAuth)と `porters.partition.search()`(partition の発見)。
54
+ - 理由: `partition` 未設定のクライアントは**`partition=0`** を全リクエストに載せていました。
55
+ `0` は PORTERS のドキュメントに存在しない値で、由来は最初の PoC の穴埋めです。実際には
56
+ Result Code **404** を招くため、**設定漏れが「サーバーが 404 を返す」形に化け**ていました。
57
+ `tenant(id)` を唯一の経路にすることで、**「partition を束ね忘れたクライアント」という状態が
58
+ 型として存在しなくなります**。実行時ガードを足すのではなく、設計で防ぐ形にしました。
59
+ - **pre-1.0 のため minor**。誤った partition を渡すこと自体は妨げません(404 はサーバーが答えます)。
60
+
61
+ ### Fixed
62
+
63
+ - **`itemstate: "existing"` を明示指定したとき、省略せずそのまま送る**ようになりました
64
+ ([ADR-0057][adr57])。**返るデータは変わりません** — 変わるのは送信 URL だけです。
65
+ - これまでは `existing` が API の既定であることを理由に、明示指定でも param を省いていました。
66
+ しかし**省略(「既定に委ねる」)と明示(「生存レコードのみが欲しい」)は別の意思表示**です。
67
+ いま結果が同じなのは PORTERS の既定が `existing` だからで、**もし将来この既定が変われば、
68
+ 生存のみを求めたコードに削除済みが混ざり始めます**(例外も Result Code も出ないので気づけません)。
69
+ - 生存のみであることが業務上重要なら、省略せず `itemstate: "existing"` と書いてください。
70
+ 省略した場合はこれまでどおり API の既定に従います。
71
+ - `itemstate=existing` の実機送信実績はまだありません([live-verification][lv] LV-15)。
72
+
73
+ ## [0.9.0] - 2026-08-20
74
+
75
+ **Candidate でメモ・住所詳細が扱えるようになった版**です。標準項目でありながらカタログから漏れていた
76
+ 4 項目を追加し、同じ取りこぼしが二度と起きないよう reference との突合を CI で検査するようにしました。
77
+ 併せて `defineFields` と Read クエリの**利用ガイドを新設**しています。
78
+
79
+ ### Added
80
+
81
+ - **Candidate に標準 4 項目**(`P_Memo`(メモ)/`P_Street`(住所詳細)/`P_Fax`(FAX)/`P_PhaseMemo`(フェーズメモ))。
82
+ カタログが型の真実源([ADR-0019][adr19])なので、これまで **`Candidate` 型にも `CandidateCreateInput` /
83
+ `CandidateUpdateInput` にも現れず**、`candidate.create({ P_Memo: "…" })` は型エラーでした。
84
+ `field` 省略時の既定 field([ADR-0020][adr20])にも含まれないため **Read でも取得されません**でした。
85
+ 同じ 4 種は Client には最初から載っており、リソース間で非一貫でした。
86
+ - `field` を省略した Read は**既定 field が 4 つ増えるぶん URL が長くなります**(Candidate で約 100 文字)。
87
+ - **公開ジェネリクスの制約型を export**(`DeclaredCatalogs` / `CustomFor` / `CustomFieldResource`)。
88
+ `PortersClient<C>` / `TenantScope<C>` は公開型なのに制約側が非公開で、
89
+ クライアントを引数に取るヘルパーの型を**名前で書けません**でした(`typeof porters` での回避のみ)。
90
+ 型の追加のみで、既存コードへの影響はありません。
91
+ - **利用ガイドを 2 本新設**([ADR-0035][adr35] の型)。
92
+ - [カスタム項目ガイド][custom-fields-guide] — `defineFields` の宣言、**宣言しないとどうなるか**
93
+ (型が付かず `field` 省略時に取得されない)、宣言できる 11 の Data Type、
94
+ Field Read でテナントの項目を調べる手順、複数テナントでの使い分け。
95
+ - [Read クエリ ガイド][read-query-guide] — `field` の 3 通りの意味、**Data Type ごとの演算子一覧**、
96
+ 削除済み Read の制約(condition は 3 項目・90 日以内)、送信前に落ちるものの一覧。
97
+
98
+ ### Changed
99
+
100
+ - **Read の `count` が範囲外なら送信前に落ちます**。`count` は PORTERS 上 **1〜200**(既定 10)ですが、
101
+ これまで範囲外の値をそのまま送っており、不透明なサーバー応答に倒れていました。
102
+ 今後は `PortersConfigError`(`category: "config"` + `searchAll()` を案内する `hint`)で**リクエストの前に**弾きます。
103
+ 整数でない値(`1.5` など)も同様です。
104
+ - keywords の 100 文字・itemstate の condition 制限・リクエスト長 ~15000 字と同じ
105
+ 「早く・明確に落とす」系列に揃いました。データ系・マスタ系・Attachment の**すべての Read 経路**に効きます。
106
+ - `count` を省略した場合の挙動は変わりません(API 既定に委ねます)。
107
+
108
+ ### Fixed
109
+
110
+ - **reference と静的カタログの突合を CI で検査**するようになりました。値を持つ標準項目の取りこぼし・
111
+ カタログ側の幻の項目・Field Type → Data Type の取り違えを検出します。
112
+ 上記 Candidate の欠落は **0.1.0 から 12 版・563 テストをすべて素通り**していたため、
113
+ 人の目ではなく仕組みで守ります。
114
+
8
115
  ## [0.8.0] - 2026-08-14
9
116
 
10
117
  **「0 件」と「届いていない」を区別できるようにした版**です。HTTP 200 を返す中間装置
@@ -241,9 +348,20 @@
241
348
  [adr51]: docs/adr/0051-read-envelope-identification.md
242
349
  [adr47]: docs/adr/0047-access-point-scheme.md
243
350
  [oauth-guide]: docs/guide/oauth.md
351
+ [adr19]: docs/adr/0019-static-resource-types.md
352
+ [adr20]: docs/adr/0020-read-field-default.md
353
+ [adr35]: docs/adr/0035-usage-documentation-structure.md
354
+ [custom-fields-guide]: docs/guide/custom-fields.md
355
+ [read-query-guide]: docs/guide/read-query.md
356
+ [adr55]: docs/adr/0055-partition-binding-guard.md
357
+ [adr56]: docs/adr/0056-deleted-flag-typing.md
358
+ [adr57]: docs/adr/0057-itemstate-existing-explicit.md
359
+ [lv]: docs/live-verification.md
244
360
  [kac]: https://keepachangelog.com/en/1.1.0/
245
361
  [semver]: https://semver.org/
246
- [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.8.0...HEAD
362
+ [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.10.0...HEAD
363
+ [0.10.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.9.0...v0.10.0
364
+ [0.9.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.8.0...v0.9.0
247
365
  [0.8.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.7.0...v0.8.0
248
366
  [0.7.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.6.2...v0.7.0
249
367
  [0.6.2]: https://github.com/Joymerrevent/porters-connect/compare/v0.6.1...v0.6.2
package/README.md CHANGED
@@ -58,15 +58,14 @@ const porters = new PortersClient({
58
58
  scheme: process.env.PORTERS_SCHEME === "http" ? "http" : undefined,
59
59
  appId: process.env.PORTERS_APP_ID!,
60
60
  appSecret: process.env.PORTERS_APP_SECRET!,
61
- partition: 123, // 既定 Partition(Company DB)Id
62
61
  });
63
62
 
64
- // 複数 partition を 1 つの App で扱う SaaS は porters.tenant(id) で束ねる
63
+ // partition(Company DB)は tenant で一度だけ束ねる。**単一テナントでもこの形**
64
+ // 以降は `t` をクライアントのように使う(client 直下には auth と partition マスタだけ)
65
65
  const t = porters.tenant(456);
66
- await t.candidate.search({ condition: { P_Name: { part: "鈴木" } } }); // partition=456
67
66
 
68
67
  // 検索(最大 200 件/ページ)。condition は項目の Data Type ごとに型付き
69
- const page = await porters.candidate.search({
68
+ const page = await t.candidate.search({
70
69
  condition: { P_Name: { part: "山田" } }, // テキストは part(部分一致)/ full(完全一致)
71
70
  order: [{ P_UpdateDate: "desc" }], // 並び順(数値・日時・System のみ)
72
71
  count: 50,
@@ -74,18 +73,21 @@ const page = await porters.candidate.search({
74
73
  console.log(page.total, page.items.length);
75
74
 
76
75
  // 1 件取得
77
- const one = await porters.candidate.get(10001);
76
+ const one = await t.candidate.get(10001);
78
77
  console.log(one?.P_Name);
79
78
 
80
79
  // 全件を自動ページング(200 件刻み)
81
- for await (const c of porters.candidate.searchAll({
80
+ for await (const c of t.candidate.searchAll({
82
81
  condition: { P_Prefecture: { full: "東京都" } },
83
82
  })) {
84
83
  // c は 1 件ずつ
85
84
  }
86
85
  ```
87
86
 
88
- > 複数 partition を 1 つの App で扱う(マルチテナント SaaS)場合の `porters.tenant(id)` スコープ・テナント別 client・partition 発見は [マルチテナント ガイド][multi-tenancy] にまとめています。
87
+ > **`partition` はクライアントに持たせません**(ADR-0055)。PORTERS は partition スコープの全リクエストで
88
+ > `partition` を要求するので、`tenant(id)` で**明示的に一度だけ**束ねる形にしています。
89
+ > 複数 partition を 1 つの App で扱う場合・テナント別 client・partition の発見は
90
+ > [マルチテナント ガイド][multi-tenancy] にまとめています。
89
91
  >
90
92
  > 認証情報やホスト名は**コミットしない**でください。`.env.example` を参考に `.env` で渡します。
91
93
 
@@ -103,7 +105,6 @@ const porters = new PortersClient({
103
105
  host: "sandbox.invalid",
104
106
  appId: "demo",
105
107
  appSecret: "demo",
106
- partition: 1,
107
108
  transport: createMockTransport((req) =>
108
109
  req.url.includes("/v1/candidate")
109
110
  ? `<Candidate Total="1" Count="1" Start="0"><Code>0</Code><Item><Person.P_Id>1</Person.P_Id><Person.P_Name>山田 太郎</Person.P_Name></Item></Candidate>`
@@ -111,7 +112,7 @@ const porters = new PortersClient({
111
112
  ), // 未モックのリクエストは明示エラー(フェイルセーフ)
112
113
  });
113
114
 
114
- const page = await porters.candidate.search();
115
+ const page = await t.candidate.search();
115
116
  console.log(page.items[0]?.P_Name); // 山田 太郎
116
117
  ```
117
118
 
@@ -141,7 +142,7 @@ const tokenStore: TokenStore = {
141
142
  /* 破棄 */
142
143
  },
143
144
  };
144
- new PortersClient({ host, appId, appSecret, partition, tokenStore });
145
+ new PortersClient({ host, appId, appSecret, tokenStore });
145
146
  ```
146
147
 
147
148
  `transport`(HTTP 注入)や `auth`(独自 `TokenProvider`)も差し替え可能です。
@@ -166,14 +167,14 @@ await porters.auth.exchangeAuthorizationCode(code);
166
167
 
167
168
  すべてのデータ系リソースは同じ形のアクセサを持ちます。
168
169
 
169
- | アクセサ | リソース | メソッド |
170
- | -------------------- | ------------ | ---------------------------------------------------- |
171
- | `porters.candidate` | 個人連絡先 | `search` / `searchAll` / `get` / `create` / `update` |
172
- | `porters.job` | JOB | `search` / `searchAll` / `get` / `create` / `update` |
173
- | `porters.client` | 企業 | `search` / `searchAll` / `get` / `create` / `update` |
174
- | `porters.process` | 選考プロセス | `search` / `searchAll` / `get` / `create` / `update` |
175
- | `porters.resume` | レジュメ | `search` / `searchAll` / `get` / `create` / `update` |
176
- | `porters.attachment` | 添付ファイル | `search` / `get` / `create` / `update` |
170
+ | アクセサ | リソース | メソッド |
171
+ | -------------- | ------------ | ---------------------------------------------------- |
172
+ | `t.candidate` | 個人連絡先 | `search` / `searchAll` / `get` / `create` / `update` |
173
+ | `t.job` | JOB | `search` / `searchAll` / `get` / `create` / `update` |
174
+ | `t.client` | 企業 | `search` / `searchAll` / `get` / `create` / `update` |
175
+ | `t.process` | 選考プロセス | `search` / `searchAll` / `get` / `create` / `update` |
176
+ | `t.resume` | レジュメ | `search` / `searchAll` / `get` / `create` / `update` |
177
+ | `t.attachment` | 添付ファイル | `search` / `get` / `create` / `update` |
177
178
 
178
179
  - `search(query?)` → `{ items, total, count, start }`(オフセット式ページング)。
179
180
  - `searchAll(query?)` → `AsyncIterable`(200 件刻みで全件 yield)。
@@ -192,10 +193,39 @@ await porters.auth.exchangeAuthorizationCode(code);
192
193
  - `order`:並び順。`[{ 項目: "asc" | "desc" }]`(数値・日時・System 型のみ)。
193
194
  - `keywords`:テキスト項目のキーワード AND 検索(`string[]`・カンマ込み **100 文字まで**)。
194
195
  - `itemstate`:`"existing"`(既定)/ `"deleted"` / `"all"`。削除済みデータの取得。
195
- - `count`(1–200・既定 10)、`start`(0 始まり)。
196
+ - `count`(1–200・既定 10)、`start`(0 始まり)。**範囲外の `count` は送信前に `PortersConfigError`** で落ちます。
196
197
 
197
198
  > **削除 API はありません**(PORTERS 仕様)。`delete()` メソッドは提供しません。削除済みは `itemstate: "deleted"` で読みます
198
199
  > (`condition` は `P_Id` / `P_UpdateDate` / `P_UpdatedBy` に限られ、更新日は 90 日以内)。
200
+ >
201
+ > `field` の 3 通りの意味(省略=全項目 / `[]`=主キーのみ / 明示)、Data Type ごとの演算子一覧、
202
+ > 削除済み Read の制約、送信前に落ちる条件は [Read クエリ ガイド][read-query-guide] にまとめています。
203
+
204
+ ### カスタム項目(`U_` / `A_`)
205
+
206
+ テナント固有のカスタム項目は `defineFields` で宣言すると、**読み書きの型に現れ**、Data Type どおりに変換されます。
207
+
208
+ ```ts
209
+ import { PortersClient, defineFields } from "@joymerrevent/porters-connect";
210
+
211
+ const fields = defineFields({
212
+ candidate: (f) => ({ U_score: f.number(), U_source: f.option() }),
213
+ });
214
+ const porters = new PortersClient({
215
+ host,
216
+ appId,
217
+ appSecret,
218
+ fields,
219
+ });
220
+
221
+ const one = await t.candidate.get(10001);
222
+ one?.U_score; // number | null | undefined
223
+ ```
224
+
225
+ 宣言しなくても読み書きはできますが(生の文字列として通ります)、**型が付かず、`field` 省略時に取得もされません**。
226
+
227
+ > 宣言できる型の一覧、テナントの項目を Field Read で調べる方法、複数テナントでの扱いは
228
+ > [カスタム項目 ガイド][custom-fields-guide] にまとめています。
199
229
 
200
230
  ### マスタ Read(読み取り専用)
201
231
 
@@ -204,25 +234,25 @@ await porters.auth.exchangeAuthorizationCode(code);
204
234
  | アクセサ | リソース | メソッド | 主なクエリ |
205
235
  | ------------------- | ----------------- | ---------------------------------- | ----------------------------------------- |
206
236
  | `porters.partition` | Partition | `search` / `searchAll` | `requestType`(1=アクセス可能一覧・既定) |
207
- | `porters.user` | User | `search` / `searchAll` / `current` | `requestType` / `userType` / `field` |
208
- | `porters.field` | Field(項目定義) | `search` / `searchAll` | `resource`(必須)/ `active` |
209
- | `porters.option` | Option(選択肢) | `search` | `alias` / `level` / `enabled` |
237
+ | `t.user` | User | `search` / `searchAll` / `current` | `requestType` / `userType` / `field` |
238
+ | `t.field` | Field(項目定義) | `search` / `searchAll` | `resource`(必須)/ `active` |
239
+ | `t.option` | Option(選択肢) | `search` | `alias` / `level` / `enabled` |
210
240
 
211
241
  ```ts
212
242
  // アクセス可能な Partition(Company DB)を発見
213
243
  const partitions = await porters.partition.search();
214
244
 
215
245
  // 現在の API ユーザー(code_direct ではアプリ自身の User)=自己同定
216
- const me = await porters.user.current();
246
+ const me = await t.user.current();
217
247
 
218
248
  // Job リソースの項目定義(U_/A_ カスタム項目を含む)を取得
219
- const fields = await porters.field.search({ resource: "job" });
249
+ const fields = await t.field.search({ resource: "job" });
220
250
 
221
251
  // 選択肢マスタをフラットな配列で取得(親子は P_ParentId で復元)
222
- const options = await porters.option.search({ alias: "Option.P_Gender" });
252
+ const options = await t.option.search({ alias: "Option.P_Gender" });
223
253
  ```
224
254
 
225
- - `porters.option.search()` は入れ子の選択肢ツリーを**深さ優先でフラット化**して返します(全ノード・`P_ParentId`/`P_Order` で階層復元可)。`start` が無いため `searchAll` はありません。
255
+ - `t.option.search()` は入れ子の選択肢ツリーを**深さ優先でフラット化**して返します(全ノード・`P_ParentId`/`P_Order` で階層復元可)。`start` が無いため `searchAll` はありません。
226
256
  - `porters.partition.current()` は提供しません(`request_type=0` は既定の `code_direct` 認証では 403 になるため)。一覧は `search()`(既定 `requestType: 1`)で取得します。
227
257
 
228
258
  ## 読み取り値の表現
@@ -242,7 +272,7 @@ PORTERS の Field Type に応じてデコードされます。
242
272
  | 空・未設定 | `null` |
243
273
 
244
274
  ```ts
245
- const c = await porters.candidate.get(10001);
275
+ const c = await t.candidate.get(10001);
246
276
  c?.P_Id; // number
247
277
  c?.P_Name; // string | null
248
278
  c?.P_RegistrationDate; // "2026-01-02T03:04:05Z" | null
@@ -265,17 +295,17 @@ c?.P_Owner; // { P_Id, P_Name, ... } | null
265
295
 
266
296
  ```ts
267
297
  // 作成(新規)
268
- const newId = await porters.candidate.create({
298
+ const newId = await t.candidate.create({
269
299
  P_Owner: 5, // User 項目は id
270
300
  P_Name: "鈴木 一郎",
271
301
  P_Reading: "すずき いちろう",
272
302
  });
273
303
 
274
304
  // 更新
275
- await porters.candidate.update(newId, { P_Mail: "ichiro@example.com" });
305
+ await t.candidate.update(newId, { P_Mail: "ichiro@example.com" });
276
306
 
277
307
  // 選考プロセス(関連 id を指定)
278
- await porters.process.create({
308
+ await t.process.create({
279
309
  P_Owner: 1,
280
310
  P_Client: 100,
281
311
  P_Recruiter: 200,
@@ -285,7 +315,7 @@ await porters.process.create({
285
315
  });
286
316
 
287
317
  // 一括作成/更新(200 件+サイズで自動分割・部分成功を返す)
288
- const bulk = await porters.candidate.createMany([
318
+ const bulk = await t.candidate.createMany([
289
319
  { P_Owner: 5, P_Name: "山田 太郎" },
290
320
  { P_Owner: 5, P_Name: "鈴木 花子" },
291
321
  ]);
@@ -301,7 +331,7 @@ if (bulk.hasFailures) console.warn(bulk.failed); // per-item は throw せず結
301
331
  ```ts
302
332
  import { bytesToBase64 } from "@joymerrevent/porters-connect";
303
333
 
304
- const id = await porters.attachment.create({
334
+ const id = await t.attachment.create({
305
335
  resource: 17, // 関連リソース種別コード(Resource List 参照)
306
336
  resourceId: 10001, // 関連レコードの id
307
337
  contentType: "application/pdf",
@@ -321,7 +351,7 @@ const id = await porters.attachment.create({
321
351
  import { PortersError } from "@joymerrevent/porters-connect";
322
352
 
323
353
  try {
324
- await porters.candidate.get(1);
354
+ await t.candidate.get(1);
325
355
  } catch (e) {
326
356
  if (e instanceof PortersError) {
327
357
  e.category; // "auth" | "permission" | "notFound" | "conflict" | "network" | ...
@@ -335,7 +365,7 @@ try {
335
365
 
336
366
  - トークン失効は内側で自動回復します。設定ミスは `PortersConfigError` で早期に落とします。
337
367
  - **`Promise` を返す公開メソッドは同期 throw しません**([ADR-0046][adr46])。設定ミスも含め常に **reject** で届くので、
338
- `porters.candidate.search(q).catch(handler)` でも捕まえられます(`string` を返す `auth.authorizationUrl` や
368
+ `t.candidate.search(q).catch(handler)` でも捕まえられます(`string` を返す `auth.authorizationUrl` や
339
369
  コンストラクタなど、Promise を返さない API は同期 throw のままです)。
340
370
  - 一時エラー・ネットワークは内蔵リトライ。非冪等な `create` はネットワーク不確実時に握り潰さず表面化します。
341
371
  - レート制限超過時、PORTERS は判別可能なコードを返さず接続を切るため、`PortersNetworkError`(category `"network"`)として表面化します。
@@ -407,6 +437,8 @@ try {
407
437
  [auth-flow]: ./docs/reference/authentication-api/README.md
408
438
  [oauth-guide]: ./docs/guide/oauth.md
409
439
  [error-handling]: ./docs/guide/error-handling.md
440
+ [custom-fields-guide]: ./docs/guide/custom-fields.md
441
+ [read-query-guide]: ./docs/guide/read-query.md
410
442
  [multi-tenancy]: ./docs/guide/multi-tenancy.md
411
443
  [bulk-write]: ./docs/guide/bulk-write.md
412
444
  [sandbox]: ./examples/offline-sandbox.ts
package/dist/index.d.ts CHANGED
@@ -138,12 +138,12 @@ type UserRef = {
138
138
  P_Mail: string | null;
139
139
  };
140
140
  type FieldValue = string | number | string[] | UserRef | null;
141
- type DecodedValue<D extends DataType> = D extends "System[Id]" | "Number" | "System[Reference]" ? number : D extends "User" ? UserRef : D extends "Option" ? string[] : string;
141
+ type DecodedValue<D extends DataType | null> = D extends null ? string : D extends "System[Id]" | "Number" | "System[Reference]" ? number : D extends "User" ? UserRef : D extends "Option" ? string[] : string;
142
142
 
143
143
  type WritableDataType = Exclude<DataType, "System[Id]" | "System[DateTime]">;
144
- type WriteValueOf<D extends DataType> = D extends "User" | "System[Reference]" | "Number" ? number : D extends "Option" ? string[] : string;
144
+ type WriteValueOf<D extends DataType | null> = D extends null ? never : D extends "User" | "System[Reference]" | "Number" ? number : D extends "Option" ? string[] : string;
145
145
 
146
- type FieldCatalog = Record<string, DataType>;
146
+ type FieldCatalog = Record<string, DataType | null>;
147
147
  /**
148
148
  * "No custom fields": the intersection-identity default for the generic catalog params (ADR-0023).
149
149
  * A bare `{}` is flagged by `no-empty-object-type`; `Record<never, never>` is the same empty object,
@@ -208,8 +208,12 @@ type ReferenceCondition = {
208
208
  or?: number[];
209
209
  and?: number[];
210
210
  };
211
- /** The condition-operator object a field of Data Type `D` accepts. */
212
- type ConditionFor<D extends DataType> = D extends "System[Id]" ? IdCondition : D extends "Number" ? NumberCondition : D extends "DateTime" | "System[DateTime]" | "Date" | "Age" ? TemporalCondition : D extends "SinglelineText" | "MultilineText" | "Mail" | "Telephone" | "URL" ? TextCondition : D extends "Option" ? OptionCondition : D extends "User" | "System[Reference]" ? ReferenceCondition : never;
211
+ /**
212
+ * The condition-operator object a field of Data Type `D` accepts. A field PORTERS gives no Data
213
+ * Type (`null` — ADR-0056) falls through to the closing `never`, which is exactly right: the
214
+ * reference says such a field cannot appear in `condition` at all.
215
+ */
216
+ type ConditionFor<D extends DataType | null> = D extends "System[Id]" ? IdCondition : D extends "Number" ? NumberCondition : D extends "DateTime" | "System[DateTime]" | "Date" | "Age" ? TemporalCondition : D extends "SinglelineText" | "MultilineText" | "Mail" | "Telephone" | "URL" ? TextCondition : D extends "Option" ? OptionCondition : D extends "User" | "System[Reference]" ? ReferenceCondition : never;
213
217
  /**
214
218
  * A typed search condition over a catalog: each field maps to the operator object its Data Type
215
219
  * allows (ADR-0038 案1a). Multiple fields are AND-joined (reference). Unknown aliases / wrong
@@ -229,10 +233,15 @@ type OrderableKeys<F extends FieldCatalog> = {
229
233
  */
230
234
  type Order<F extends FieldCatalog> = Array<Partial<Record<OrderableKeys<F>, "asc" | "desc">>>;
231
235
  /**
232
- * Which delete state to read. Omitting (or `existing`) reads live data; `deleted`/`all` read deleted
233
- * records — the only way to read deleted data, since there is no delete API. When `deleted`/`all`,
234
- * condition is restricted to `P_Id` / `P_UpdateDate` / `P_UpdatedBy` and PORTERS auto-adds a
235
- * "updated within 90 days" filter (`P_UpdateDate` = the delete time, `P_UpdatedBy` = the last editor).
236
+ * Which delete state to read. `existing` reads live data, `deleted`/`all` read deleted records —
237
+ * the only way to read deleted data, since there is no delete API. When `deleted`/`all`, condition
238
+ * is restricted to `P_Id` / `P_UpdateDate` / `P_UpdatedBy` and PORTERS auto-adds a "updated within
239
+ * 90 days" filter (`P_UpdateDate` = the delete time, `P_UpdatedBy` = the last editor).
240
+ *
241
+ * **Omitting the field is not the same as passing `existing`** (ADR-0057). Omitting defers to the
242
+ * API's own default (today: `existing`); passing `existing` states that you want live records only,
243
+ * and is sent as such. Both read live data now, but only the explicit form keeps doing so if PORTERS
244
+ * ever changes that default. Use `itemstate: "existing"` when live-only actually matters to you.
236
245
  */
237
246
  type ItemState = "existing" | "deleted" | "all";
238
247
  type SearchQuery<F extends FieldCatalog = FieldCatalog> = {
@@ -332,16 +341,21 @@ declare const FIELDS$8: {
332
341
  readonly P_UpdatedBy: "User";
333
342
  readonly P_Phase: "Option";
334
343
  readonly P_PhaseDate: "DateTime";
344
+ readonly P_PhaseMemo: "MultilineText";
335
345
  readonly P_Name: "SinglelineText";
336
346
  readonly P_Reading: "SinglelineText";
347
+ readonly P_Memo: "MultilineText";
337
348
  readonly P_Mail: "Mail";
338
349
  readonly P_MobileMail: "Mail";
339
350
  readonly P_Telephone: "Telephone";
340
351
  readonly P_Mobile: "Telephone";
352
+ readonly P_Fax: "Telephone";
341
353
  readonly P_Country: "SinglelineText";
342
354
  readonly P_Prefecture: "SinglelineText";
343
355
  readonly P_City: "SinglelineText";
356
+ readonly P_Street: "MultilineText";
344
357
  readonly P_Zipcode: "SinglelineText";
358
+ readonly P_Deleted: null;
345
359
  };
346
360
  declare const REQUIRED_ON_CREATE$4: readonly ["P_Owner"];
347
361
  /** A decoded Candidate: known `P_` fields, each requested field `value | null`. */
@@ -390,6 +404,7 @@ declare const FIELDS$7: {
390
404
  readonly P_CapitalText: "SinglelineText";
391
405
  readonly P_EmploymentType: "Option";
392
406
  readonly P_ExpectedAgeReason: "Option";
407
+ readonly P_Deleted: null;
393
408
  };
394
409
  declare const REQUIRED_ON_CREATE$3: readonly ["P_Owner", "P_Client", "P_Recruiter"];
395
410
  /** A decoded Job: known `P_` fields, each requested field `value | null`. */
@@ -422,6 +437,7 @@ declare const FIELDS$6: {
422
437
  readonly P_Zipcode: "SinglelineText";
423
438
  readonly P_Telephone: "Telephone";
424
439
  readonly P_Fax: "Telephone";
440
+ readonly P_Deleted: null;
425
441
  };
426
442
  declare const REQUIRED_ON_CREATE$2: readonly ["P_Owner"];
427
443
  /** A decoded Client (company): known `P_` fields, each requested field `value | null`. */
@@ -454,6 +470,7 @@ declare const FIELDS$5: {
454
470
  readonly P_CloseReason: "Option";
455
471
  readonly P_ExpectedSalesAmount: "Number";
456
472
  readonly P_ExpectedClosingDate: "Date";
473
+ readonly P_Deleted: null;
457
474
  };
458
475
  declare const REQUIRED_ON_CREATE$1: readonly ["P_Owner", "P_Client", "P_Recruiter", "P_Job", "P_Candidate", "P_Resume"];
459
476
  /** A decoded Process (a Candidate's progress through a Job): known `P_` fields, each
@@ -499,6 +516,7 @@ declare const FIELDS$4: {
499
516
  readonly P_ExpectSalary: "Number";
500
517
  readonly P_DesiredHourlyRate: "Number";
501
518
  readonly P_HourlyRate: "Number";
519
+ readonly P_Deleted: null;
502
520
  };
503
521
  declare const REQUIRED_ON_CREATE: readonly ["P_Owner", "P_Candidate"];
504
522
  /** A decoded Resume (a Candidate's CV / profile): known `P_` fields, each requested field
@@ -775,12 +793,6 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
775
793
  appId?: string;
776
794
  appSecret?: string;
777
795
  scopes?: Scope[];
778
- /**
779
- * Default partition (Company DB) for every call. For multi-tenant routing, bind a partition
780
- * per call with {@link PortersClient.tenant} (ADR-0040 / F-3); for a fully separated per-partition
781
- * token, construct a dedicated client per tenant instead (ADR-0008 案3).
782
- */
783
- partition?: PartitionId;
784
796
  /** Custom auth strategy; defaults to the transparent code_direct strategy. */
785
797
  auth?: TokenProvider;
786
798
  /** Token persistence; defaults to in-memory. */
@@ -796,8 +808,10 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
796
808
  };
797
809
  /**
798
810
  * The partition-bound resource accessors returned by {@link PortersClient.tenant} (ADR-0040 / F-3).
799
- * Every accessor here routes to the bound tenant's partition. `auth` (App-level), the `partition`
800
- * master (discovery — partition-less), and `tenant` itself (no nesting) are deliberately absent.
811
+ * **This is the only way to reach a partition-scoped resource** (ADR-0055): PORTERS requires
812
+ * `partition` on every one of these calls, so the API makes you supply it exactly once, explicitly.
813
+ * `auth` (App-level), the `partition` master (discovery — partition-less), and `tenant` itself
814
+ * (no nesting) are deliberately absent: none of them takes a partition.
801
815
  */
802
816
  type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
803
817
  readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
@@ -811,36 +825,41 @@ type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
811
825
  readonly option: OptionResource;
812
826
  };
813
827
  /**
814
- * Entry point of the library. Wires the default transport / auth / throttle /
815
- * requester and exposes namespaced resource accessors such as `candidate`
816
- * (ADR-0005).
828
+ * Entry point of the library. Wires the default transport / auth / throttle / requester and exposes
829
+ * the **App-level** surface: `auth`, the `partition` master (discovery), and {@link PortersClient.tenant}.
830
+ *
831
+ * Everything that PORTERS scopes to a partition (Company DB) lives behind `tenant(id)` — see
832
+ * {@link TenantScope}. The client holds no default partition (ADR-0055): a partition is bound
833
+ * explicitly, exactly once, so "unbound" is not a state this API can be in.
834
+ *
835
+ * @example
836
+ * const porters = new PortersClient({ host, appId, appSecret });
837
+ * await porters.auth.ensureAuthenticated(); // App-level
838
+ * const t = porters.tenant(123); // bind the partition once
839
+ * const page = await t.candidate.search();
817
840
  */
818
841
  declare class PortersClient<C extends DeclaredCatalogs = EmptyCatalog> {
819
842
  #private;
820
- readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
821
- readonly job: JobResource<CustomFor<C, "job">>;
822
- readonly client: ClientResource<CustomFor<C, "client">>;
823
- readonly process: ProcessResource<CustomFor<C, "process">>;
824
- readonly resume: ResumeResource<CustomFor<C, "resume">>;
825
- readonly attachment: AttachmentResource;
826
843
  /** OAuth surface: initial browser grant, token warm-up/inspection, local revoke (ADR-0007/0034). */
827
844
  readonly auth: AuthApi;
828
- /** Master Read: accessible partitions (ADR-0021/0022). */
845
+ /**
846
+ * Master Read: the partitions this App can reach (ADR-0021/0022). Takes no `partition` itself —
847
+ * it is how you *discover* the ids to pass to {@link PortersClient.tenant}.
848
+ */
829
849
  readonly partition: PartitionResource;
830
- /** Master Read: users, plus `current()` self-identification (ADR-0021/0022). */
831
- readonly user: UserResource;
832
- /** Master Read: a resource's field catalog (ADR-0021/0022). */
833
- readonly field: FieldResource;
834
- /** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
835
- readonly option: OptionResource;
836
850
  /**
837
- * Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
838
- * scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
839
- * `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
840
- * the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
841
- * User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
842
- * and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
843
- * token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
851
+ * Bind a partition (Company DB) and get the accessors that route through it (ADR-0040 F-3).
852
+ * **Single-tenant apps use this too** — it is the only path to a partition-scoped resource
853
+ * (ADR-0055). Hold the scope once and use it like a client:
854
+ *
855
+ * ```ts
856
+ * const t = porters.tenant(123);
857
+ * await t.candidate.search();
858
+ * ```
859
+ *
860
+ * `auth` (App-level), the `partition` master (discovery — takes no partition), and `tenant`
861
+ * itself (no nesting) are intentionally absent from the returned scope. For a fully separated
862
+ * per-partition token, construct a dedicated {@link PortersClient} per tenant (ADR-0008 案3).
844
863
  */
845
864
  readonly tenant: (id: PartitionId) => TenantScope<C>;
846
865
  constructor(options: PortersClientOptions<C>);
@@ -901,4 +920,4 @@ declare const bytesToBase64: (bytes: Uint8Array) => string;
901
920
  /** Decode a Base64 string back to raw bytes. */
902
921
  declare const base64ToBytes: (b64: string) => Uint8Array;
903
922
 
904
- export { type Attachment, type AttachmentCreate, type AttachmentPage, type AttachmentResource, type AttachmentSearchQuery, type AttachmentUpdate, type AuthApi, type AuthorizationUrlOptions, type BulkWriteResult, type BulkWriteResultItem, type Candidate, type CandidateCreateInput, type CandidatePage, type CandidateResource, type CandidateSearchQuery, type CandidateUpdateInput, type Client, type ClientCreateInput, type ClientPage, type ClientResource, type ClientSearchQuery, type ClientUpdateInput, type Condition, type CustomDataType, type DefinedFields, type ErrorCategory, type Field, type FieldBuilder, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldValue, type GetAccessTokenOptions, type ItemState, type Job, type JobCreateInput, type JobPage, type JobResource, type JobSearchQuery, type JobUpdateInput, type MockHandler, type MockReply, type MockTransportOptions, type Option, type OptionResource, type OptionSearchQuery, type Order, type Partition, type PartitionId, type PartitionPage, type PartitionResource, type PartitionSearchQuery, PortersAuthError, PortersClient, type PortersClientOptions, PortersConfigError, PortersError, type PortersErrorContext, type PortersErrorOptions, PortersNetworkError, PortersResourceError, type Process, type ProcessCreateInput, type ProcessPage, type ProcessResource, type ProcessSearchQuery, type ProcessUpdateInput, type ResourceType, type Resume, type ResumeCreateInput, type ResumePage, type ResumeResource, type ResumeSearchQuery, type ResumeUpdateInput, type RevokeUrlOptions, type Scheme, type Scope, type SearchQuery, type StoredTokens, type TenantScope, type TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, base64ToBytes, bytesToBase64, createMockTransport, defineFields };
923
+ export { type Attachment, type AttachmentCreate, type AttachmentPage, type AttachmentResource, type AttachmentSearchQuery, type AttachmentUpdate, type AuthApi, type AuthorizationUrlOptions, type BulkWriteResult, type BulkWriteResultItem, type Candidate, type CandidateCreateInput, type CandidatePage, type CandidateResource, type CandidateSearchQuery, type CandidateUpdateInput, type Client, type ClientCreateInput, type ClientPage, type ClientResource, type ClientSearchQuery, type ClientUpdateInput, type Condition, type CustomDataType, type CustomFieldResource, type CustomFor, type DeclaredCatalogs, type DefinedFields, type ErrorCategory, type Field, type FieldBuilder, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldValue, type GetAccessTokenOptions, type ItemState, type Job, type JobCreateInput, type JobPage, type JobResource, type JobSearchQuery, type JobUpdateInput, type MockHandler, type MockReply, type MockTransportOptions, type Option, type OptionResource, type OptionSearchQuery, type Order, type Partition, type PartitionId, type PartitionPage, type PartitionResource, type PartitionSearchQuery, PortersAuthError, PortersClient, type PortersClientOptions, PortersConfigError, PortersError, type PortersErrorContext, type PortersErrorOptions, PortersNetworkError, PortersResourceError, type Process, type ProcessCreateInput, type ProcessPage, type ProcessResource, type ProcessSearchQuery, type ProcessUpdateInput, type ResourceType, type Resume, type ResumeCreateInput, type ResumePage, type ResumeResource, type ResumeSearchQuery, type ResumeUpdateInput, type RevokeUrlOptions, type Scheme, type Scope, type SearchQuery, type StoredTokens, type TenantScope, type TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, base64ToBytes, bytesToBase64, createMockTransport, defineFields };