@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 +34 -1
- package/README.md +21 -8
- package/dist/index.d.ts +120 -11
- package/dist/index.js +133 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
//
|
|
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: {
|
|
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: {
|
|
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`:検索条件。`{
|
|
181
|
-
`eq`/`gt`/`ge`/`le`/`lt
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
757
|
-
|
|
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:
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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() {
|