@joymerrevent/porters-connect 0.9.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,71 @@
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
+
8
73
  ## [0.9.0] - 2026-08-20
9
74
 
10
75
  **Candidate でメモ・住所詳細が扱えるようになった版**です。標準項目でありながらカタログから漏れていた
@@ -288,9 +353,14 @@
288
353
  [adr35]: docs/adr/0035-usage-documentation-structure.md
289
354
  [custom-fields-guide]: docs/guide/custom-fields.md
290
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
291
360
  [kac]: https://keepachangelog.com/en/1.1.0/
292
361
  [semver]: https://semver.org/
293
- [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.9.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
294
364
  [0.9.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.8.0...v0.9.0
295
365
  [0.8.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.7.0...v0.8.0
296
366
  [0.7.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.6.2...v0.7.0
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)。
@@ -214,11 +215,10 @@ const porters = new PortersClient({
214
215
  host,
215
216
  appId,
216
217
  appSecret,
217
- partition,
218
218
  fields,
219
219
  });
220
220
 
221
- const one = await porters.candidate.get(10001);
221
+ const one = await t.candidate.get(10001);
222
222
  one?.U_score; // number | null | undefined
223
223
  ```
224
224
 
@@ -234,25 +234,25 @@ one?.U_score; // number | null | undefined
234
234
  | アクセサ | リソース | メソッド | 主なクエリ |
235
235
  | ------------------- | ----------------- | ---------------------------------- | ----------------------------------------- |
236
236
  | `porters.partition` | Partition | `search` / `searchAll` | `requestType`(1=アクセス可能一覧・既定) |
237
- | `porters.user` | User | `search` / `searchAll` / `current` | `requestType` / `userType` / `field` |
238
- | `porters.field` | Field(項目定義) | `search` / `searchAll` | `resource`(必須)/ `active` |
239
- | `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` |
240
240
 
241
241
  ```ts
242
242
  // アクセス可能な Partition(Company DB)を発見
243
243
  const partitions = await porters.partition.search();
244
244
 
245
245
  // 現在の API ユーザー(code_direct ではアプリ自身の User)=自己同定
246
- const me = await porters.user.current();
246
+ const me = await t.user.current();
247
247
 
248
248
  // Job リソースの項目定義(U_/A_ カスタム項目を含む)を取得
249
- const fields = await porters.field.search({ resource: "job" });
249
+ const fields = await t.field.search({ resource: "job" });
250
250
 
251
251
  // 選択肢マスタをフラットな配列で取得(親子は P_ParentId で復元)
252
- const options = await porters.option.search({ alias: "Option.P_Gender" });
252
+ const options = await t.option.search({ alias: "Option.P_Gender" });
253
253
  ```
254
254
 
255
- - `porters.option.search()` は入れ子の選択肢ツリーを**深さ優先でフラット化**して返します(全ノード・`P_ParentId`/`P_Order` で階層復元可)。`start` が無いため `searchAll` はありません。
255
+ - `t.option.search()` は入れ子の選択肢ツリーを**深さ優先でフラット化**して返します(全ノード・`P_ParentId`/`P_Order` で階層復元可)。`start` が無いため `searchAll` はありません。
256
256
  - `porters.partition.current()` は提供しません(`request_type=0` は既定の `code_direct` 認証では 403 になるため)。一覧は `search()`(既定 `requestType: 1`)で取得します。
257
257
 
258
258
  ## 読み取り値の表現
@@ -272,7 +272,7 @@ PORTERS の Field Type に応じてデコードされます。
272
272
  | 空・未設定 | `null` |
273
273
 
274
274
  ```ts
275
- const c = await porters.candidate.get(10001);
275
+ const c = await t.candidate.get(10001);
276
276
  c?.P_Id; // number
277
277
  c?.P_Name; // string | null
278
278
  c?.P_RegistrationDate; // "2026-01-02T03:04:05Z" | null
@@ -295,17 +295,17 @@ c?.P_Owner; // { P_Id, P_Name, ... } | null
295
295
 
296
296
  ```ts
297
297
  // 作成(新規)
298
- const newId = await porters.candidate.create({
298
+ const newId = await t.candidate.create({
299
299
  P_Owner: 5, // User 項目は id
300
300
  P_Name: "鈴木 一郎",
301
301
  P_Reading: "すずき いちろう",
302
302
  });
303
303
 
304
304
  // 更新
305
- await porters.candidate.update(newId, { P_Mail: "ichiro@example.com" });
305
+ await t.candidate.update(newId, { P_Mail: "ichiro@example.com" });
306
306
 
307
307
  // 選考プロセス(関連 id を指定)
308
- await porters.process.create({
308
+ await t.process.create({
309
309
  P_Owner: 1,
310
310
  P_Client: 100,
311
311
  P_Recruiter: 200,
@@ -315,7 +315,7 @@ await porters.process.create({
315
315
  });
316
316
 
317
317
  // 一括作成/更新(200 件+サイズで自動分割・部分成功を返す)
318
- const bulk = await porters.candidate.createMany([
318
+ const bulk = await t.candidate.createMany([
319
319
  { P_Owner: 5, P_Name: "山田 太郎" },
320
320
  { P_Owner: 5, P_Name: "鈴木 花子" },
321
321
  ]);
@@ -331,7 +331,7 @@ if (bulk.hasFailures) console.warn(bulk.failed); // per-item は throw せず結
331
331
  ```ts
332
332
  import { bytesToBase64 } from "@joymerrevent/porters-connect";
333
333
 
334
- const id = await porters.attachment.create({
334
+ const id = await t.attachment.create({
335
335
  resource: 17, // 関連リソース種別コード(Resource List 参照)
336
336
  resourceId: 10001, // 関連レコードの id
337
337
  contentType: "application/pdf",
@@ -351,7 +351,7 @@ const id = await porters.attachment.create({
351
351
  import { PortersError } from "@joymerrevent/porters-connect";
352
352
 
353
353
  try {
354
- await porters.candidate.get(1);
354
+ await t.candidate.get(1);
355
355
  } catch (e) {
356
356
  if (e instanceof PortersError) {
357
357
  e.category; // "auth" | "permission" | "notFound" | "conflict" | "network" | ...
@@ -365,7 +365,7 @@ try {
365
365
 
366
366
  - トークン失効は内側で自動回復します。設定ミスは `PortersConfigError` で早期に落とします。
367
367
  - **`Promise` を返す公開メソッドは同期 throw しません**([ADR-0046][adr46])。設定ミスも含め常に **reject** で届くので、
368
- `porters.candidate.search(q).catch(handler)` でも捕まえられます(`string` を返す `auth.authorizationUrl` や
368
+ `t.candidate.search(q).catch(handler)` でも捕まえられます(`string` を返す `auth.authorizationUrl` や
369
369
  コンストラクタなど、Promise を返さない API は同期 throw のままです)。
370
370
  - 一時エラー・ネットワークは内蔵リトライ。非冪等な `create` はネットワーク不確実時に握り潰さず表面化します。
371
371
  - レート制限超過時、PORTERS は判別可能なコードを返さず接続を切るため、`PortersNetworkError`(category `"network"`)として表面化します。
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> = {
@@ -346,6 +355,7 @@ declare const FIELDS$8: {
346
355
  readonly P_City: "SinglelineText";
347
356
  readonly P_Street: "MultilineText";
348
357
  readonly P_Zipcode: "SinglelineText";
358
+ readonly P_Deleted: null;
349
359
  };
350
360
  declare const REQUIRED_ON_CREATE$4: readonly ["P_Owner"];
351
361
  /** A decoded Candidate: known `P_` fields, each requested field `value | null`. */
@@ -394,6 +404,7 @@ declare const FIELDS$7: {
394
404
  readonly P_CapitalText: "SinglelineText";
395
405
  readonly P_EmploymentType: "Option";
396
406
  readonly P_ExpectedAgeReason: "Option";
407
+ readonly P_Deleted: null;
397
408
  };
398
409
  declare const REQUIRED_ON_CREATE$3: readonly ["P_Owner", "P_Client", "P_Recruiter"];
399
410
  /** A decoded Job: known `P_` fields, each requested field `value | null`. */
@@ -426,6 +437,7 @@ declare const FIELDS$6: {
426
437
  readonly P_Zipcode: "SinglelineText";
427
438
  readonly P_Telephone: "Telephone";
428
439
  readonly P_Fax: "Telephone";
440
+ readonly P_Deleted: null;
429
441
  };
430
442
  declare const REQUIRED_ON_CREATE$2: readonly ["P_Owner"];
431
443
  /** A decoded Client (company): known `P_` fields, each requested field `value | null`. */
@@ -458,6 +470,7 @@ declare const FIELDS$5: {
458
470
  readonly P_CloseReason: "Option";
459
471
  readonly P_ExpectedSalesAmount: "Number";
460
472
  readonly P_ExpectedClosingDate: "Date";
473
+ readonly P_Deleted: null;
461
474
  };
462
475
  declare const REQUIRED_ON_CREATE$1: readonly ["P_Owner", "P_Client", "P_Recruiter", "P_Job", "P_Candidate", "P_Resume"];
463
476
  /** A decoded Process (a Candidate's progress through a Job): known `P_` fields, each
@@ -503,6 +516,7 @@ declare const FIELDS$4: {
503
516
  readonly P_ExpectSalary: "Number";
504
517
  readonly P_DesiredHourlyRate: "Number";
505
518
  readonly P_HourlyRate: "Number";
519
+ readonly P_Deleted: null;
506
520
  };
507
521
  declare const REQUIRED_ON_CREATE: readonly ["P_Owner", "P_Candidate"];
508
522
  /** A decoded Resume (a Candidate's CV / profile): known `P_` fields, each requested field
@@ -779,12 +793,6 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
779
793
  appId?: string;
780
794
  appSecret?: string;
781
795
  scopes?: Scope[];
782
- /**
783
- * Default partition (Company DB) for every call. For multi-tenant routing, bind a partition
784
- * per call with {@link PortersClient.tenant} (ADR-0040 / F-3); for a fully separated per-partition
785
- * token, construct a dedicated client per tenant instead (ADR-0008 案3).
786
- */
787
- partition?: PartitionId;
788
796
  /** Custom auth strategy; defaults to the transparent code_direct strategy. */
789
797
  auth?: TokenProvider;
790
798
  /** Token persistence; defaults to in-memory. */
@@ -800,8 +808,10 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
800
808
  };
801
809
  /**
802
810
  * The partition-bound resource accessors returned by {@link PortersClient.tenant} (ADR-0040 / F-3).
803
- * Every accessor here routes to the bound tenant's partition. `auth` (App-level), the `partition`
804
- * 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.
805
815
  */
806
816
  type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
807
817
  readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
@@ -815,36 +825,41 @@ type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
815
825
  readonly option: OptionResource;
816
826
  };
817
827
  /**
818
- * Entry point of the library. Wires the default transport / auth / throttle /
819
- * requester and exposes namespaced resource accessors such as `candidate`
820
- * (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();
821
840
  */
822
841
  declare class PortersClient<C extends DeclaredCatalogs = EmptyCatalog> {
823
842
  #private;
824
- readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
825
- readonly job: JobResource<CustomFor<C, "job">>;
826
- readonly client: ClientResource<CustomFor<C, "client">>;
827
- readonly process: ProcessResource<CustomFor<C, "process">>;
828
- readonly resume: ResumeResource<CustomFor<C, "resume">>;
829
- readonly attachment: AttachmentResource;
830
843
  /** OAuth surface: initial browser grant, token warm-up/inspection, local revoke (ADR-0007/0034). */
831
844
  readonly auth: AuthApi;
832
- /** 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
+ */
833
849
  readonly partition: PartitionResource;
834
- /** Master Read: users, plus `current()` self-identification (ADR-0021/0022). */
835
- readonly user: UserResource;
836
- /** Master Read: a resource's field catalog (ADR-0021/0022). */
837
- readonly field: FieldResource;
838
- /** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
839
- readonly option: OptionResource;
840
850
  /**
841
- * Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
842
- * scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
843
- * `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
844
- * the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
845
- * User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
846
- * and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
847
- * 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).
848
863
  */
849
864
  readonly tenant: (id: PartitionId) => TenantScope<C>;
850
865
  constructor(options: PortersClientOptions<C>);
package/dist/index.js CHANGED
@@ -740,7 +740,7 @@ var encodeItem = (prefix, fields, item) => {
740
740
  for (const [alias, value] of Object.entries(item)) {
741
741
  if (value === null || value === void 0) continue;
742
742
  const type = fields.get(alias);
743
- const inner = type === void 0 ? scalar(value) : encodeField(type, value);
743
+ const inner = type === void 0 || type === null ? scalar(value) : encodeField(type, value);
744
744
  parts.push(`<${prefix}.${alias}>${inner}</${prefix}.${alias}>`);
745
745
  }
746
746
  return parts.join("");
@@ -786,6 +786,7 @@ var decodeReference = (raw) => {
786
786
  };
787
787
  var decodeField = (type, raw) => {
788
788
  if (raw === "" || raw === void 0 || raw === null) return null;
789
+ if (type === null) return asString(raw) ?? null;
789
790
  switch (type) {
790
791
  case "System[Id]":
791
792
  case "Number": {
@@ -832,7 +833,7 @@ var decoderFor = (fields) => {
832
833
  for (const [key, raw] of Object.entries(item)) {
833
834
  const alias = bareAlias(key);
834
835
  const type = fieldMap.get(alias);
835
- out[alias] = type ? decodeField(type, raw) : typeof raw === "string" ? raw : null;
836
+ out[alias] = type === void 0 ? typeof raw === "string" ? raw : null : decodeField(type, raw);
836
837
  }
837
838
  return out;
838
839
  };
@@ -946,9 +947,7 @@ var appendReadQuery = (p, q, ctx) => {
946
947
  }
947
948
  p.set("keywords", kw);
948
949
  }
949
- if (q.itemstate !== void 0 && q.itemstate !== "existing") {
950
- p.set("itemstate", q.itemstate);
951
- }
950
+ if (q.itemstate !== void 0) p.set("itemstate", q.itemstate);
952
951
  };
953
952
 
954
953
  // src/resources/bulk-write.ts
@@ -1069,7 +1068,9 @@ var buildWriteUrl = (accessPoint, partition, path) => apiUrl(
1069
1068
  new URLSearchParams({ partition: String(partition) })
1070
1069
  );
1071
1070
  var createResource = (config, deps) => {
1072
- const fieldMap = new Map(Object.entries(config.fields));
1071
+ const fieldMap = new Map(
1072
+ Object.entries(config.fields)
1073
+ );
1073
1074
  const decode = decoderFor(config.fields);
1074
1075
  const defaultFields = defaultFieldList(config.prefix, config.fields);
1075
1076
  const readUrl = (q) => buildReadUrl(deps.accessPoint, deps.partition, config.path, q, {
@@ -1151,7 +1152,12 @@ var FIELDS = {
1151
1152
  P_Prefecture: "SinglelineText",
1152
1153
  P_City: "SinglelineText",
1153
1154
  P_Street: "MultilineText",
1154
- P_Zipcode: "SinglelineText"
1155
+ P_Zipcode: "SinglelineText",
1156
+ // 削除状態("0" 生存 / "1" 削除済み)。PORTERS はこの項目に Data Type を与えていない
1157
+ // (reference の Field Type / Data Type とも「ー」)ので `null`=型が無いことを記録する(ADR-0056)。
1158
+ // 「未設定」でも「不明」でもない。null は condition / order / Write の型から自動的に外れる=
1159
+ // reference の「Read の field でのみ指定可」がそのまま型で守られる。
1160
+ P_Deleted: null
1155
1161
  };
1156
1162
  var REQUIRED_ON_CREATE = [
1157
1163
  "P_Owner"
@@ -1206,7 +1212,10 @@ var FIELDS2 = {
1206
1212
  P_EstablishmentDateText: "SinglelineText",
1207
1213
  P_CapitalText: "SinglelineText",
1208
1214
  P_EmploymentType: "Option",
1209
- P_ExpectedAgeReason: "Option"
1215
+ P_ExpectedAgeReason: "Option",
1216
+ // 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
1217
+ // 詳細は candidate.ts のコメント)。Read の field でのみ指定でき、型もそう振る舞う。
1218
+ P_Deleted: null
1210
1219
  };
1211
1220
  var REQUIRED_ON_CREATE2 = [
1212
1221
  "P_Owner",
@@ -1246,7 +1255,10 @@ var FIELDS3 = {
1246
1255
  P_Street: "MultilineText",
1247
1256
  P_Zipcode: "SinglelineText",
1248
1257
  P_Telephone: "Telephone",
1249
- P_Fax: "Telephone"
1258
+ P_Fax: "Telephone",
1259
+ // 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
1260
+ // 詳細は candidate.ts のコメント)。Read の field でのみ指定でき、型もそう振る舞う。
1261
+ P_Deleted: null
1250
1262
  };
1251
1263
  var REQUIRED_ON_CREATE3 = [
1252
1264
  "P_Owner"
@@ -1284,7 +1296,10 @@ var FIELDS4 = {
1284
1296
  P_Close: "Option",
1285
1297
  P_CloseReason: "Option",
1286
1298
  P_ExpectedSalesAmount: "Number",
1287
- P_ExpectedClosingDate: "Date"
1299
+ P_ExpectedClosingDate: "Date",
1300
+ // 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
1301
+ // 詳細は candidate.ts のコメント)。Read の field でのみ指定でき、型もそう振る舞う。
1302
+ P_Deleted: null
1288
1303
  };
1289
1304
  var REQUIRED_ON_CREATE4 = [
1290
1305
  "P_Owner",
@@ -1339,7 +1354,10 @@ var FIELDS5 = {
1339
1354
  P_ExpectCondition: "MultilineText",
1340
1355
  P_ExpectSalary: "Number",
1341
1356
  P_DesiredHourlyRate: "Number",
1342
- P_HourlyRate: "Number"
1357
+ P_HourlyRate: "Number",
1358
+ // 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
1359
+ // 詳細は candidate.ts のコメント)。Read の field でのみ指定でき、型もそう振る舞う。
1360
+ P_Deleted: null
1343
1361
  };
1344
1362
  var REQUIRED_ON_CREATE5 = [
1345
1363
  "P_Owner",
@@ -1637,30 +1655,26 @@ var createOptionResource = (deps) => {
1637
1655
 
1638
1656
  // src/client.ts
1639
1657
  var PortersClient = class {
1640
- candidate;
1641
- job;
1642
- client;
1643
- process;
1644
- resume;
1645
- attachment;
1646
1658
  /** OAuth surface: initial browser grant, token warm-up/inspection, local revoke (ADR-0007/0034). */
1647
1659
  auth;
1648
- /** Master Read: accessible partitions (ADR-0021/0022). */
1660
+ /**
1661
+ * Master Read: the partitions this App can reach (ADR-0021/0022). Takes no `partition` itself —
1662
+ * it is how you *discover* the ids to pass to {@link PortersClient.tenant}.
1663
+ */
1649
1664
  partition;
1650
- /** Master Read: users, plus `current()` self-identification (ADR-0021/0022). */
1651
- user;
1652
- /** Master Read: a resource's field catalog (ADR-0021/0022). */
1653
- field;
1654
- /** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
1655
- option;
1656
1665
  /**
1657
- * Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
1658
- * scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
1659
- * `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
1660
- * the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
1661
- * User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
1662
- * and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
1663
- * token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
1666
+ * Bind a partition (Company DB) and get the accessors that route through it (ADR-0040 F-3).
1667
+ * **Single-tenant apps use this too** — it is the only path to a partition-scoped resource
1668
+ * (ADR-0055). Hold the scope once and use it like a client:
1669
+ *
1670
+ * ```ts
1671
+ * const t = porters.tenant(123);
1672
+ * await t.candidate.search();
1673
+ * ```
1674
+ *
1675
+ * `auth` (App-level), the `partition` master (discovery — takes no partition), and `tenant`
1676
+ * itself (no nesting) are intentionally absent from the returned scope. For a fully separated
1677
+ * per-partition token, construct a dedicated {@link PortersClient} per tenant (ADR-0008 案3).
1664
1678
  */
1665
1679
  tenant;
1666
1680
  #accessPoint;
@@ -1719,16 +1733,6 @@ var PortersClient = class {
1719
1733
  };
1720
1734
  };
1721
1735
  this.tenant = buildScope;
1722
- const root = buildScope(options.partition ?? 0);
1723
- this.candidate = root.candidate;
1724
- this.job = root.job;
1725
- this.client = root.client;
1726
- this.process = root.process;
1727
- this.resume = root.resume;
1728
- this.attachment = root.attachment;
1729
- this.user = root.user;
1730
- this.field = root.field;
1731
- this.option = root.option;
1732
1736
  this.partition = createPartitionResource({ requester, accessPoint });
1733
1737
  }
1734
1738
  /** The configured API host. */