@joymerrevent/porters-connect 0.3.0 → 0.5.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,37 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.0] - 2026-06-29
9
+
10
+ ### Added
11
+
12
+ - **マルチテナント面**(F-3 / ADR-0040・ADR-0008/0021)。`porters.tenant(id)` で partition(Company DB)を
13
+ 束ねたアクセサ群 `TenantScope` を返します。`porters.tenant(123).candidate.search(...)` は `partition=123`
14
+ を毎回付与し、未束ねの呼び出しは **client 既定 `partition`** を使います(解決は tenant スコープ/client 既定の 2 層)。
15
+ - スコープは data(candidate/job/client/process/resume)+ attachment + master Read(user/field/option)を露出。
16
+ `auth`(App 単位)・`partition` マスタ(発見専用・partition 非送信)・`tenant`(ネスト不可)は含みません。
17
+ - partition 別トークンが必要な場合は**テナント別 `PortersClient`** を構築(ADR-0008 案3)。
18
+ - 新規 export 型 `TenantScope`。`PortersClientOptions.partition` の JSDoc を更新(per-call 引数は設けない方針)。
19
+
20
+ ## [0.4.0] - 2026-06-27
21
+
22
+ ### Added
23
+
24
+ - **Read クエリ面の拡充**(F-2 / ADR-0038・ADR-0005 R-5)。データ系の `search` / `searchAll` が次を受けます。
25
+ - `order` — 並び順 `[{ 項目: "asc" | "desc" }]`(数値・日時・System 型のみ)。
26
+ - `keywords` — テキスト項目の AND キーワード検索(`string[]`・カンマ込み 100 文字まで。超過は送信前に `PortersConfigError`)。
27
+ - `itemstate` — `"existing"`(既定)/ `"deleted"` / `"all"`。削除 API 非提供下で**削除済みデータを読む唯一の手段**
28
+ (`condition` は `P_Id` / `P_UpdateDate` / `P_UpdatedBy` に限定・実行時ガード/更新日 90 日以内は PORTERS 側が自動付与)。
29
+ - 新規 export 型 `Condition` / `Order` / `ItemState` / `SearchQuery`。
30
+
31
+ ### Changed
32
+
33
+ - **(破壊的)`condition` を型安全化**。`{ "Person.P_Name:part": "山田" }`(loose な `Record<string,string>`)から
34
+ **`{ P_Name: { part: "山田" } }`**(項目の Data Type が許す演算子だけを受ける型付き形・ADR-0038 案1a)へ変更。
35
+ 日時の値は ISO 8601(UTC `…Z`)で渡すと PORTERS 形式へ自動変換。**pre-1.0 のため minor**。
36
+ - 移行: キーを `"Alias:suffix"` から**接頭辞なしの項目名**へ、値を `{ suffix: 値 }` へ。
37
+ 例 `{ "Person.P_Id:eq": "1" }` → `{ P_Id: { eq: 1 } }`。テキスト項目は `eq` 不可(`part` / `full`)。
38
+
8
39
  ## [0.3.0] - 2026-06-23
9
40
 
10
41
  ### Added
@@ -81,7 +112,9 @@
81
112
  [oauth-guide]: docs/guide/oauth.md
82
113
  [kac]: https://keepachangelog.com/en/1.1.0/
83
114
  [semver]: https://semver.org/
84
- [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.3.0...HEAD
115
+ [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.5.0...HEAD
116
+ [0.5.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.4.0...v0.5.0
117
+ [0.4.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.3.0...v0.4.0
85
118
  [0.3.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.2.1...v0.3.0
86
119
  [0.2.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.2.0...v0.2.1
87
120
  [0.2.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.1.1...v0.2.0
package/README.md CHANGED
@@ -56,12 +56,17 @@ const porters = new PortersClient({
56
56
  host: process.env.PORTERS_HOST!, // 契約時に通知される値。ハードコード禁止
57
57
  appId: process.env.PORTERS_APP_ID!,
58
58
  appSecret: process.env.PORTERS_APP_SECRET!,
59
- partition: 123, // Partition(Company DB)Id
59
+ partition: 123, // 既定 Partition(Company DB)Id
60
60
  });
61
61
 
62
- // 検索(最大 200 件/ページ)
62
+ // 複数 partition を 1 つの App で扱う SaaS は porters.tenant(id) で束ねる
63
+ const t = porters.tenant(456);
64
+ await t.candidate.search({ condition: { P_Name: { part: "鈴木" } } }); // partition=456
65
+
66
+ // 検索(最大 200 件/ページ)。condition は項目の Data Type ごとに型付き
63
67
  const page = await porters.candidate.search({
64
- condition: { "Person.P_Name:part": "山田" }, // 部分一致
68
+ condition: { P_Name: { part: "山田" } }, // テキストは part(部分一致)/ full(完全一致)
69
+ order: [{ P_UpdateDate: "desc" }], // 並び順(数値・日時・System のみ)
65
70
  count: 50,
66
71
  });
67
72
  console.log(page.total, page.items.length);
@@ -72,12 +77,14 @@ console.log(one?.P_Name);
72
77
 
73
78
  // 全件を自動ページング(200 件刻み)
74
79
  for await (const c of porters.candidate.searchAll({
75
- condition: { "Person.P_Prefecture:eq": "東京都" },
80
+ condition: { P_Prefecture: { full: "東京都" } },
76
81
  })) {
77
82
  // c は 1 件ずつ
78
83
  }
79
84
  ```
80
85
 
86
+ > 複数 partition を 1 つの App で扱う(マルチテナント SaaS)場合の `porters.tenant(id)` スコープ・テナント別 client・partition 発見は [マルチテナント ガイド][multi-tenancy] にまとめています。
87
+ >
81
88
  > 認証情報やホスト名は**コミットしない**でください。`.env.example` を参考に `.env` で渡します。
82
89
 
83
90
  ## 契約なしで試す(オフライン評価)
@@ -172,16 +179,21 @@ await porters.auth.exchangeAuthorizationCode(code);
172
179
  - `create(input)` → 採番された **id(number)**。
173
180
  - `update(id, input)` → その **id**。
174
181
 
175
- **検索クエリ**(`query`)の主なキー:
182
+ **検索クエリ**(`query`)の主なキー(すべて型安全。**項目の Data Type が許す演算子だけ**を受けます):
176
183
 
177
184
  - `field`:取得する項目(接頭辞付き alias の配列。例 `["Person.P_Id", "Person.P_Name"]`)。
178
185
  **省略するとカタログ上の全項目を既定取得**します(PORTERS は field 未指定だと主キーのみ返すため、
179
186
  ライブラリが既定 field を補います)。`field: []`(空配列)を渡すと API 仕様どおり**主キーのみ**を返します(件数取得など)。
180
- - `condition`:検索条件。`{ "[Alias]:[suffix]": "値" }` 形式。`suffix` は型ごとに
181
- `eq`/`gt`/`ge`/`le`/`lt`(数値・日時)、`part`/`full`(テキスト)、`or`/`and`(Option)。
187
+ - `condition`:検索条件。`{ 項目: { 演算子: 値 } }` 形式(複数項目は AND)。演算子は Data Type ごとに
188
+ `eq`/`gt`/`ge`/`le`/`lt`(数値・日時・Id)、`part`/`full`(テキスト)、`or`/`and`(Option・参照/ユーザー型は ID)。
189
+ **日時の値は ISO 8601(UTC `…Z`)**で渡すと PORTERS 形式へ自動変換します。
190
+ - `order`:並び順。`[{ 項目: "asc" | "desc" }]`(数値・日時・System 型のみ)。
191
+ - `keywords`:テキスト項目のキーワード AND 検索(`string[]`・カンマ込み **100 文字まで**)。
192
+ - `itemstate`:`"existing"`(既定)/ `"deleted"` / `"all"`。削除済みデータの取得。
182
193
  - `count`(1–200・既定 10)、`start`(0 始まり)。
183
194
 
184
- > **削除 API はありません**(PORTERS 仕様)。`delete()` メソッドは提供しません。削除済みは検索の状態フィルタで扱います。
195
+ > **削除 API はありません**(PORTERS 仕様)。`delete()` メソッドは提供しません。削除済みは `itemstate: "deleted"` で読みます
196
+ > (`condition` は `P_Id` / `P_UpdateDate` / `P_UpdatedBy` に限られ、更新日は 90 日以内)。
185
197
 
186
198
  ### マスタ Read(読み取り専用)
187
199
 
@@ -359,6 +371,7 @@ try {
359
371
  [auth-flow]: ./docs/reference/authentication-api/README.md
360
372
  [oauth-guide]: ./docs/guide/oauth.md
361
373
  [error-handling]: ./docs/guide/error-handling.md
374
+ [multi-tenancy]: ./docs/guide/multi-tenancy.md
362
375
  [sandbox]: ./examples/offline-sandbox.ts
363
376
  [adr]: ./docs/adr/README.md
364
377
  [design]: ./docs/design/basic-design.md
package/dist/index.d.ts CHANGED
@@ -161,7 +161,75 @@ type ResourcePage<F extends FieldCatalog> = {
161
161
  start: number;
162
162
  };
163
163
 
164
- type SearchQuery = {
164
+ /** Comparable ops for numeric Ids (System[Id]); `or` matches a set of Resource Ids (`P_Id:or=1:2`). */
165
+ type IdCondition = {
166
+ gt?: number;
167
+ ge?: number;
168
+ eq?: number;
169
+ le?: number;
170
+ lt?: number;
171
+ or?: number[];
172
+ };
173
+ /** Comparable ops for Number. */
174
+ type NumberCondition = {
175
+ gt?: number;
176
+ ge?: number;
177
+ eq?: number;
178
+ le?: number;
179
+ lt?: number;
180
+ };
181
+ /** Comparable ops for date/time fields. Values are ISO 8601 (UTC `…Z`); normalised to PORTERS on send. */
182
+ type TemporalCondition = {
183
+ gt?: string;
184
+ ge?: string;
185
+ eq?: string;
186
+ le?: string;
187
+ lt?: string;
188
+ };
189
+ /** Text match. `full` = exact, `part` = substring (PORTERS default). */
190
+ type TextCondition = {
191
+ full?: string;
192
+ part?: string;
193
+ };
194
+ /** Option-select match; values are option aliases (e.g. `Option.P_SE`), OR/AND-joined. */
195
+ type OptionCondition = {
196
+ or?: string[];
197
+ and?: string[];
198
+ };
199
+ /** Link/reference match by id (User / System[Reference]): `eq` one id, or OR/AND a set of ids. */
200
+ type ReferenceCondition = {
201
+ eq?: number;
202
+ or?: number[];
203
+ and?: number[];
204
+ };
205
+ /** The condition-operator object a field of Data Type `D` accepts. */
206
+ 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;
207
+ /**
208
+ * A typed search condition over a catalog: each field maps to the operator object its Data Type
209
+ * allows (ADR-0038 案1a). Multiple fields are AND-joined (reference). Unknown aliases / wrong
210
+ * operators are type errors. Custom `U_`/`A_` fields are not in the catalog — condition on them via
211
+ * a cast (the encoder passes unknown aliases through as raw scalars, like read/write).
212
+ */
213
+ type Condition<F extends FieldCatalog> = {
214
+ [K in keyof F]?: ConditionFor<F[K]>;
215
+ };
216
+ type OrderableDataType = "System[Id]" | "System[DateTime]" | "Number" | "DateTime" | "Date" | "Age";
217
+ type OrderableKeys<F extends FieldCatalog> = {
218
+ [K in keyof F]: F[K] extends OrderableDataType ? K : never;
219
+ }[keyof F];
220
+ /**
221
+ * Sort spec: an ordered list of `{ field: "asc" | "desc" }`, encoded in array (then key) order. Only
222
+ * orderable Data Types (Number/Date/DateTime/Age/System) are accepted (reference).
223
+ */
224
+ type Order<F extends FieldCatalog> = Array<Partial<Record<OrderableKeys<F>, "asc" | "desc">>>;
225
+ /**
226
+ * Which delete state to read. Omitting (or `existing`) reads live data; `deleted`/`all` read deleted
227
+ * records — the only way to read deleted data, since there is no delete API. When `deleted`/`all`,
228
+ * condition is restricted to `P_Id` / `P_UpdateDate` / `P_UpdatedBy` and PORTERS auto-adds a
229
+ * "updated within 90 days" filter (`P_UpdateDate` = the delete time, `P_UpdatedBy` = the last editor).
230
+ */
231
+ type ItemState = "existing" | "deleted" | "all";
232
+ type SearchQuery<F extends FieldCatalog = FieldCatalog> = {
165
233
  /**
166
234
  * Output fields as prefixed aliases (e.g. `Person.P_Name`). **Omit** to fetch every catalogued
167
235
  * field by default (ADR-0020): PORTERS returns only the primary key for a fieldless request, so
@@ -169,10 +237,21 @@ type SearchQuery = {
169
237
  * API-native "primary key only" response (e.g. counting). A non-empty list is sent verbatim.
170
238
  */
171
239
  field?: string[];
172
- condition?: Record<string, string>;
240
+ /** Typed AND-conditions; each field's operators derive from its Data Type (ADR-0038). */
241
+ condition?: Condition<F>;
242
+ /** Sort order; orderable Data Types only (Number/Date/DateTime/Age/System). */
243
+ order?: Order<F>;
244
+ /**
245
+ * Keyword AND-search over text fields (MultilineText/SinglelineText/Mail/URL; Telephone digits
246
+ * only). OR is not supported. Max 100 characters including commas — guarded before send.
247
+ */
248
+ keywords?: string[];
249
+ /** Delete-state filter (default `existing`). `deleted`/`all` restrict `condition` — see {@link ItemState}. */
250
+ itemstate?: ItemState;
173
251
  count?: number;
174
252
  start?: number;
175
253
  };
254
+
176
255
  type WritableKeys<F extends FieldCatalog> = {
177
256
  [K in keyof F]: F[K] extends WritableDataType ? K : never;
178
257
  }[keyof F];
@@ -190,9 +269,9 @@ type UpdateInput<F extends FieldCatalog> = {
190
269
  [K in WritableKeys<F>]?: WriteValueOf<F[K]> | null;
191
270
  };
192
271
  type Resource<F extends FieldCatalog, Req extends keyof F> = {
193
- search(query?: SearchQuery): Promise<ResourcePage<F>>;
272
+ search(query?: SearchQuery<F>): Promise<ResourcePage<F>>;
194
273
  /** Auto-paginating search: yields every matching record (200 per page). */
195
- searchAll(query?: Omit<SearchQuery, "count" | "start">): AsyncIterable<ReadRecord<F>>;
274
+ searchAll(query?: Omit<SearchQuery<F>, "count" | "start">): AsyncIterable<ReadRecord<F>>;
196
275
  get(id: number): Promise<ReadRecord<F> | undefined>;
197
276
  /** Create one record; resolves to the newly assigned id. */
198
277
  create(input: CreateInput<F, Req>): Promise<number>;
@@ -224,7 +303,7 @@ declare const REQUIRED_ON_CREATE$4: readonly ["P_Owner"];
224
303
  /** A decoded Candidate: known `P_` fields, each requested field `value | null`. */
225
304
  type Candidate = ReadRecord<typeof FIELDS$8>;
226
305
  type CandidatePage = ResourcePage<typeof FIELDS$8>;
227
- type CandidateSearchQuery = SearchQuery;
306
+ type CandidateSearchQuery = SearchQuery<typeof FIELDS$8>;
228
307
  /** Fields for `create`: `P_Owner` is required; `P_Id` / system timestamps are not settable. */
229
308
  type CandidateCreateInput = CreateInput<typeof FIELDS$8, (typeof REQUIRED_ON_CREATE$4)[number]>;
230
309
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -272,7 +351,7 @@ declare const REQUIRED_ON_CREATE$3: readonly ["P_Owner", "P_Client", "P_Recruite
272
351
  /** A decoded Job: known `P_` fields, each requested field `value | null`. */
273
352
  type Job = ReadRecord<typeof FIELDS$7>;
274
353
  type JobPage = ResourcePage<typeof FIELDS$7>;
275
- type JobSearchQuery = SearchQuery;
354
+ type JobSearchQuery = SearchQuery<typeof FIELDS$7>;
276
355
  /** Fields for `create`: `P_Owner` required; `P_Id` / system timestamps are not settable. */
277
356
  type JobCreateInput = CreateInput<typeof FIELDS$7, (typeof REQUIRED_ON_CREATE$3)[number]>;
278
357
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -304,7 +383,7 @@ declare const REQUIRED_ON_CREATE$2: readonly ["P_Owner"];
304
383
  /** A decoded Client (company): known `P_` fields, each requested field `value | null`. */
305
384
  type Client = ReadRecord<typeof FIELDS$6>;
306
385
  type ClientPage = ResourcePage<typeof FIELDS$6>;
307
- type ClientSearchQuery = SearchQuery;
386
+ type ClientSearchQuery = SearchQuery<typeof FIELDS$6>;
308
387
  /** Fields for `create`: `P_Owner` required; `P_Id` / system timestamps are not settable. */
309
388
  type ClientCreateInput = CreateInput<typeof FIELDS$6, (typeof REQUIRED_ON_CREATE$2)[number]>;
310
389
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -337,7 +416,7 @@ declare const REQUIRED_ON_CREATE$1: readonly ["P_Owner", "P_Client", "P_Recruite
337
416
  * requested field `value | null`. */
338
417
  type Process = ReadRecord<typeof FIELDS$5>;
339
418
  type ProcessPage = ResourcePage<typeof FIELDS$5>;
340
- type ProcessSearchQuery = SearchQuery;
419
+ type ProcessSearchQuery = SearchQuery<typeof FIELDS$5>;
341
420
  /** Fields for `create`: `P_Owner` required; `P_Id` / system timestamps are not settable. */
342
421
  type ProcessCreateInput = CreateInput<typeof FIELDS$5, (typeof REQUIRED_ON_CREATE$1)[number]>;
343
422
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -382,7 +461,7 @@ declare const REQUIRED_ON_CREATE: readonly ["P_Owner", "P_Candidate"];
382
461
  * `value | null`. */
383
462
  type Resume = ReadRecord<typeof FIELDS$4>;
384
463
  type ResumePage = ResourcePage<typeof FIELDS$4>;
385
- type ResumeSearchQuery = SearchQuery;
464
+ type ResumeSearchQuery = SearchQuery<typeof FIELDS$4>;
386
465
  /** Fields for `create`: `P_Owner` required; `P_Id` / system timestamps are not settable. */
387
466
  type ResumeCreateInput = CreateInput<typeof FIELDS$4, (typeof REQUIRED_ON_CREATE)[number]>;
388
467
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -635,7 +714,11 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
635
714
  appId?: string;
636
715
  appSecret?: string;
637
716
  scopes?: Scope[];
638
- /** Partition (Company DB) used for every call. Per-call override is not yet supported (planned — see ADR-0033). */
717
+ /**
718
+ * Default partition (Company DB) for every call. For multi-tenant routing, bind a partition
719
+ * per call with {@link PortersClient.tenant} (ADR-0040 / F-3); for a fully separated per-partition
720
+ * token, construct a dedicated client per tenant instead (ADR-0008 案3).
721
+ */
639
722
  partition?: PartitionId;
640
723
  /** Custom auth strategy; defaults to the transparent code_direct strategy. */
641
724
  auth?: TokenProvider;
@@ -650,6 +733,22 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
650
733
  */
651
734
  fields?: DefinedFields<C>;
652
735
  };
736
+ /**
737
+ * The partition-bound resource accessors returned by {@link PortersClient.tenant} (ADR-0040 / F-3).
738
+ * Every accessor here routes to the bound tenant's partition. `auth` (App-level), the `partition`
739
+ * master (discovery — partition-less), and `tenant` itself (no nesting) are deliberately absent.
740
+ */
741
+ type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
742
+ readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
743
+ readonly job: JobResource<CustomFor<C, "job">>;
744
+ readonly client: ClientResource<CustomFor<C, "client">>;
745
+ readonly process: ProcessResource<CustomFor<C, "process">>;
746
+ readonly resume: ResumeResource<CustomFor<C, "resume">>;
747
+ readonly attachment: AttachmentResource;
748
+ readonly user: UserResource;
749
+ readonly field: FieldResource;
750
+ readonly option: OptionResource;
751
+ };
653
752
  /**
654
753
  * Entry point of the library. Wires the default transport / auth / throttle /
655
754
  * requester and exposes namespaced resource accessors such as `candidate`
@@ -673,6 +772,16 @@ declare class PortersClient<C extends DeclaredCatalogs = EmptyCatalog> {
673
772
  readonly field: FieldResource;
674
773
  /** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
675
774
  readonly option: OptionResource;
775
+ /**
776
+ * Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
777
+ * scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
778
+ * `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
779
+ * the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
780
+ * User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
781
+ * and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
782
+ * token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
783
+ */
784
+ readonly tenant: (id: PartitionId) => TenantScope<C>;
676
785
  constructor(options: PortersClientOptions<C>);
677
786
  /** The configured API host. */
678
787
  get host(): string;
@@ -726,4 +835,4 @@ declare const bytesToBase64: (bytes: Uint8Array) => string;
726
835
  /** Decode a Base64 string back to raw bytes. */
727
836
  declare const base64ToBytes: (b64: string) => Uint8Array;
728
837
 
729
- export { type Attachment, type AttachmentCreate, type AttachmentPage, type AttachmentResource, type AttachmentSearchQuery, type AttachmentUpdate, type AuthApi, type AuthorizationUrlOptions, 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 CustomDataType, type DefinedFields, type ErrorCategory, type Field, type FieldBuilder, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldValue, type GetAccessTokenOptions, 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 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 Scope, type StoredTokens, type TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, base64ToBytes, bytesToBase64, createMockTransport, defineFields };
838
+ export { type Attachment, type AttachmentCreate, type AttachmentPage, type AttachmentResource, type AttachmentSearchQuery, type AttachmentUpdate, type AuthApi, type AuthorizationUrlOptions, 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 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 };
package/dist/index.js CHANGED
@@ -703,6 +703,83 @@ var paginate = async function* (fetchPage) {
703
703
  }
704
704
  };
705
705
 
706
+ // src/resources/query.ts
707
+ var KEYWORDS_MAX_CHARS = 100;
708
+ var DELETED_CONDITION_FIELDS = /* @__PURE__ */ new Set([
709
+ "P_Id",
710
+ "P_UpdateDate",
711
+ "P_UpdatedBy"
712
+ ]);
713
+ var serializeConditionValue = (type, value) => {
714
+ if (Array.isArray(value)) return value.map(String).join(":");
715
+ if (type === "DateTime" || type === "System[DateTime]") {
716
+ return isoToPortersDateTime(String(value));
717
+ }
718
+ if (type === "Date" || type === "Age") return isoToPortersDate(String(value));
719
+ return String(value);
720
+ };
721
+ var encodeCondition = (condition, itemstate, ctx) => {
722
+ const restricted = itemstate === "deleted" || itemstate === "all";
723
+ const parts = [];
724
+ for (const [alias, ops] of Object.entries(condition)) {
725
+ if (ops === void 0) continue;
726
+ if (restricted && !DELETED_CONDITION_FIELDS.has(alias)) {
727
+ throw new PortersConfigError(
728
+ `condition field "${alias}" is not allowed when itemstate is "${itemstate}"`,
729
+ {
730
+ category: "config",
731
+ hint: "Deleted reads (itemstate deleted/all) accept only P_Id, P_UpdateDate, P_UpdatedBy in condition."
732
+ }
733
+ );
734
+ }
735
+ const type = ctx.fields.get(alias);
736
+ for (const [suffix, value] of Object.entries(ops)) {
737
+ if (value === void 0) continue;
738
+ parts.push(
739
+ `${ctx.prefix}.${alias}:${suffix}=${serializeConditionValue(type, value)}`
740
+ );
741
+ }
742
+ }
743
+ return parts.join(",");
744
+ };
745
+ var encodeOrder = (order, ctx) => {
746
+ const parts = [];
747
+ for (const spec of order) {
748
+ const dirs = spec;
749
+ for (const [alias, dir] of Object.entries(dirs)) {
750
+ if (dir === void 0) continue;
751
+ parts.push(`${ctx.prefix}.${alias}:${dir}`);
752
+ }
753
+ }
754
+ return parts.join(",");
755
+ };
756
+ var appendReadQuery = (p, q, ctx) => {
757
+ if (q.condition) {
758
+ const cond = encodeCondition(q.condition, q.itemstate, ctx);
759
+ if (cond.length > 0) p.set("condition", cond);
760
+ }
761
+ if (q.order) {
762
+ const order = encodeOrder(q.order, ctx);
763
+ if (order.length > 0) p.set("order", order);
764
+ }
765
+ if (q.keywords && q.keywords.length > 0) {
766
+ const kw = q.keywords.join(",");
767
+ if (kw.length > KEYWORDS_MAX_CHARS) {
768
+ throw new PortersConfigError(
769
+ `keywords is ${kw.length} characters, over the ${KEYWORDS_MAX_CHARS}-character limit`,
770
+ {
771
+ category: "config",
772
+ hint: "Shorten keywords: PORTERS caps the keyword search at 100 characters including commas."
773
+ }
774
+ );
775
+ }
776
+ p.set("keywords", kw);
777
+ }
778
+ if (q.itemstate !== void 0 && q.itemstate !== "existing") {
779
+ p.set("itemstate", q.itemstate);
780
+ }
781
+ };
782
+
706
783
  // src/resources/resource.ts
707
784
  var USER_SUBFIELDS = ["P_Id", "P_Type", "P_Name", "P_Mail"];
708
785
  var defaultFieldList = (prefix, fields) => Object.entries(fields).map(
@@ -726,14 +803,11 @@ var firstWriteResultId = (body, path, name) => {
726
803
  }
727
804
  return first.id;
728
805
  };
729
- var buildReadUrl = (host, partition, path, q) => {
806
+ var buildReadUrl = (host, partition, path, q, ctx) => {
730
807
  const p = new URLSearchParams();
731
808
  p.set("partition", String(partition));
732
809
  if (q.field && q.field.length > 0) p.set("field", q.field.join(","));
733
- if (q.condition) {
734
- const conds = Object.entries(q.condition).map(([k, v]) => `${k}=${v}`);
735
- if (conds.length > 0) p.set("condition", conds.join(","));
736
- }
810
+ appendReadQuery(p, q, ctx);
737
811
  if (q.count !== void 0) p.set("count", String(q.count));
738
812
  if (q.start !== void 0) p.set("start", String(q.start));
739
813
  return `https://${host}/v1/${path}?${p.toString()}`;
@@ -743,7 +817,10 @@ var createResource = (config, deps) => {
743
817
  const fieldMap = new Map(Object.entries(config.fields));
744
818
  const decode = decoderFor(config.fields);
745
819
  const defaultFields = defaultFieldList(config.prefix, config.fields);
746
- const readUrl = (q) => buildReadUrl(deps.host, deps.partition, config.path, q);
820
+ const readUrl = (q) => buildReadUrl(deps.host, deps.partition, config.path, q, {
821
+ prefix: config.prefix,
822
+ fields: fieldMap
823
+ });
747
824
  const writeUrl = () => buildWriteUrl(deps.host, deps.partition, config.path);
748
825
  const firstWriteId = (body) => firstWriteResultId(body, config.path, config.name);
749
826
  const search = (query = {}) => runRead(
@@ -753,10 +830,8 @@ var createResource = (config, deps) => {
753
830
  );
754
831
  const searchAll = (query = {}) => paginate((count, start) => search({ ...query, count, start }));
755
832
  const get = async (id) => {
756
- const page = await search({
757
- condition: { [`${config.prefix}.P_Id:eq`]: String(id) },
758
- count: 1
759
- });
833
+ const condition = { P_Id: { eq: id } };
834
+ const page = await search({ condition, count: 1 });
760
835
  return page.items[0];
761
836
  };
762
837
  const write = (item, idempotent) => deps.requester.request(
@@ -1023,6 +1098,18 @@ var DEFAULT_FIELDS = [
1023
1098
  "ContentType",
1024
1099
  "FileName"
1025
1100
  ];
1101
+ var buildAttachmentReadUrl = (host, partition, q) => {
1102
+ const p = new URLSearchParams();
1103
+ p.set("partition", String(partition));
1104
+ if (q.field && q.field.length > 0) p.set("field", q.field.join(","));
1105
+ if (q.condition) {
1106
+ const conds = Object.entries(q.condition).map(([k, v]) => `${k}=${v}`);
1107
+ if (conds.length > 0) p.set("condition", conds.join(","));
1108
+ }
1109
+ if (q.count !== void 0) p.set("count", String(q.count));
1110
+ if (q.start !== void 0) p.set("start", String(q.start));
1111
+ return `https://${host}/v1/attachment?${p.toString()}`;
1112
+ };
1026
1113
  var numOrNull = (v) => {
1027
1114
  const s = asString(v);
1028
1115
  return s === void 0 ? null : Number(s);
@@ -1048,7 +1135,7 @@ var createAttachmentResource = (deps) => {
1048
1135
  const search = (query = {}) => deps.requester.request(
1049
1136
  {
1050
1137
  method: "GET",
1051
- url: buildReadUrl(deps.host, deps.partition, "attachment", {
1138
+ url: buildAttachmentReadUrl(deps.host, deps.partition, {
1052
1139
  ...query,
1053
1140
  field: query.field ?? DEFAULT_FIELDS
1054
1141
  }),
@@ -1252,6 +1339,16 @@ var PortersClient = class {
1252
1339
  field;
1253
1340
  /** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
1254
1341
  option;
1342
+ /**
1343
+ * Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
1344
+ * scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
1345
+ * `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
1346
+ * the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
1347
+ * User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
1348
+ * and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
1349
+ * token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
1350
+ */
1351
+ tenant;
1255
1352
  #host;
1256
1353
  constructor(options) {
1257
1354
  const transport = options.transport ?? createFetchTransport();
@@ -1286,22 +1383,33 @@ var PortersClient = class {
1286
1383
  backoff: expoBackoff()
1287
1384
  });
1288
1385
  this.#host = options.host;
1289
- const deps = {
1290
- requester,
1291
- host: options.host,
1292
- partition: options.partition ?? 0
1293
- };
1294
1386
  const customFor = (key) => options.fields?.[key] ?? {};
1295
- this.candidate = createCandidateResource(deps, customFor("candidate"));
1296
- this.job = createJobResource(deps, customFor("job"));
1297
- this.client = createClientResource(deps, customFor("client"));
1298
- this.process = createProcessResource(deps, customFor("process"));
1299
- this.resume = createResumeResource(deps, customFor("resume"));
1300
- this.attachment = createAttachmentResource(deps);
1387
+ const buildScope = (partition) => {
1388
+ const deps = { requester, host: options.host, partition };
1389
+ return {
1390
+ candidate: createCandidateResource(deps, customFor("candidate")),
1391
+ job: createJobResource(deps, customFor("job")),
1392
+ client: createClientResource(deps, customFor("client")),
1393
+ process: createProcessResource(deps, customFor("process")),
1394
+ resume: createResumeResource(deps, customFor("resume")),
1395
+ attachment: createAttachmentResource(deps),
1396
+ user: createUserResource(deps),
1397
+ field: createFieldResource(deps),
1398
+ option: createOptionResource(deps)
1399
+ };
1400
+ };
1401
+ this.tenant = buildScope;
1402
+ const root = buildScope(options.partition ?? 0);
1403
+ this.candidate = root.candidate;
1404
+ this.job = root.job;
1405
+ this.client = root.client;
1406
+ this.process = root.process;
1407
+ this.resume = root.resume;
1408
+ this.attachment = root.attachment;
1409
+ this.user = root.user;
1410
+ this.field = root.field;
1411
+ this.option = root.option;
1301
1412
  this.partition = createPartitionResource({ requester, host: options.host });
1302
- this.user = createUserResource(deps);
1303
- this.field = createFieldResource(deps);
1304
- this.option = createOptionResource(deps);
1305
1413
  }
1306
1414
  /** The configured API host. */
1307
1415
  get host() {