@joymerrevent/porters-connect 0.1.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 ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ このプロジェクトの主な変更を記録します。形式は [Keep a Changelog][kac] に準拠し、
4
+ バージョニングは [Semantic Versioning][semver] に従います。
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-06-19
9
+
10
+ 初回リリース。PORTERS Connect API(旧 HRBC)を型安全・簡単に扱う**非公式** TypeScript ラッパー。
11
+ 利用には PORTERS 契約+ Connect API オプション契約が必要です。
12
+
13
+ ### Added
14
+
15
+ - **`PortersClient`**: `host` / `appId` / `appSecret` / `scopes` / `partition` で初期化する型付きクライアント。
16
+ - **OAuth(独自仕様)**: `code_direct` でのトークン取得・キャッシュ・自動リフレッシュ、差し替え可能なトークンストア(既定インメモリ)。
17
+ - **リソース(MVP)**: Candidate / Job / Client / Process / Resume の Read(`search` / `searchAll` / `get`)+ Write(`create` / `update`)、Attachment(Base64・専用アクセサ)、マスタ Read(Partition / User(+`current()`)/ Field / Option)。
18
+ - **XML の隠蔽**: レスポンス XML → 型付きオブジェクト、入力 → XML を内部生成(Option は `string[]`、User / Reference は ID、DateTime / Date を正規化)。
19
+ - **型付き検索クエリ**(`field` / `condition` / `count` / `start`)+ **自動ページング**(200 件刻みの `searchAll`)。
20
+ - **レート市民 & リトライ**: 自前スロットリング+指数バックオフ、送信前リクエストサイズガード(約 15000 文字・URL + body 合算)。
21
+ - **構造化エラー**: 基底 `PortersError` + 系統別サブクラス(Auth / Resource / Network / Config)+ `category`(11 種)。
22
+ - **日時**: ISO 8601(UTC)⇄ PORTERS 形式の正規化(業務タイムゾーン変換はしない)。
23
+ - **動的カスタム項目**: `defineFields` でテナント固有の `U_` / `A_` を宣言し、型安全に read / write(ADR-0023)。
24
+ - **評価用サンドボックス**: 公開モック `createMockTransport` で契約なし・オフライン動作(ADR-0024)。
25
+ - **エラー対処ガイド**: [docs/guide/error-handling.md][guide](症状別早見表+2 系統のコード対応表)。
26
+ - **配布**: ESM / Node.js 18+ / 型定義同梱 / MIT。`X-P-ConnectAPI-Version: 2` を既定送信(PORTERS 8.x・9.x 想定)。
27
+
28
+ [guide]: docs/guide/error-handling.md
29
+ [kac]: https://keepachangelog.com/en/1.1.0/
30
+ [semver]: https://semver.org/
31
+ [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.1.0...HEAD
32
+ [0.1.0]: https://github.com/Joymerrevent/porters-connect/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joymerrevent
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # @joymerrevent/porters-connect
2
+
3
+ [![License: MIT][mit-badge]][mit] ![Node >= 18][node-badge]
4
+
5
+ PORTERS Connect API(旧 HRBC)を **TypeScript から型安全・簡単に**扱うための、
6
+ [Joymerrevent(ジョイメリベント)][joymerrevent] 製の **非公式(unofficial)** ラッパーです。
7
+
8
+ > [!IMPORTANT]
9
+ > これは**非公式**ライブラリです。ポーターズ株式会社とは無関係で、公式ロゴ・商標は使用していません。
10
+ > 利用には **PORTERS の契約 + Connect API オプション契約**が必要です(ホスト名・App ID/Secret は契約時に通知されます)。
11
+
12
+ XML レスポンスを型付きオブジェクトに変換し、独自仕様の OAuth・レート制御・エラー整理を内側に隠します。
13
+ **薄く・堅く**を方針に、フェイルセーフ(壊れたときに安全側へ倒れる)設計です。
14
+
15
+ ---
16
+
17
+ ## 特徴
18
+
19
+ - **型安全**:リソース・フィールド値を型で表現。`any` を撒きません。
20
+ - **XML を外に出さない**:返り値は型付きオブジェクト、入力も素直な JS の値。
21
+ - **独自 OAuth を透過**:`code_direct` によるトークン取得・キャッシュ・更新を自動化。
22
+ - **良き API 市民**:自前スロットリング・リトライ(指数バックオフ)・リクエストサイズガード内蔵。
23
+ - **日時は ISO 8601(UTC)に正規化**。業務タイムゾーン変換はしません(利用側の責務)。
24
+ - **6 リソース対応**:Candidate / Job / Client / Process / Resume / Attachment(Read/Write)。
25
+ - **マスタ Read**:Partition / User / Field / Option(読み取り専用・`user.current()` で自己同定)。
26
+
27
+ ## 前提
28
+
29
+ 1. **PORTERS 契約 + Connect API オプション契約**。ホスト名・App ID・App Secret が通知されます。
30
+ 2. **初回のみブラウザで権限付与**(人手・1 回)。`response_type=code` で対象 Company DB の権限を付与します。
31
+ これを済ませれば、以降はライブラリが `code_direct`(サーバ間・ブラウザ不要)で無人運用できます。
32
+ 詳細は [認証 API のフロー][auth-flow]を参照。
33
+
34
+ ## インストール
35
+
36
+ ```sh
37
+ npm i @joymerrevent/porters-connect
38
+ # pnpm add / yarn add も可
39
+ ```
40
+
41
+ - Node.js 18+(ESM)。TypeScript 同梱の型定義。
42
+
43
+ ## クイックスタート
44
+
45
+ ```ts
46
+ import { PortersClient } from "@joymerrevent/porters-connect";
47
+
48
+ const porters = new PortersClient({
49
+ host: process.env.PORTERS_HOST!, // 契約時に通知される値。ハードコード禁止
50
+ appId: process.env.PORTERS_APP_ID!,
51
+ appSecret: process.env.PORTERS_APP_SECRET!,
52
+ partition: 123, // Partition(Company DB)Id
53
+ });
54
+
55
+ // 検索(最大 200 件/ページ)
56
+ const page = await porters.candidate.search({
57
+ condition: { "Person.P_Name:part": "山田" }, // 部分一致
58
+ count: 50,
59
+ });
60
+ console.log(page.total, page.items.length);
61
+
62
+ // 1 件取得
63
+ const one = await porters.candidate.get(10001);
64
+ console.log(one?.P_Name);
65
+
66
+ // 全件を自動ページング(200 件刻み)
67
+ for await (const c of porters.candidate.searchAll({
68
+ condition: { "Person.P_Prefecture:eq": "東京都" },
69
+ })) {
70
+ // c は 1 件ずつ
71
+ }
72
+ ```
73
+
74
+ > 認証情報やホスト名は**コミットしない**でください。`.env.example` を参考に `.env` で渡します。
75
+
76
+ ## 契約なしで試す(オフライン評価)
77
+
78
+ PORTERS 契約が無くても、公開ヘルパー `createMockTransport` にモック XML を返させれば**全機能をオフラインで**動かせます(OAuth / トークンは自動応答)。
79
+
80
+ ```ts
81
+ import {
82
+ PortersClient,
83
+ createMockTransport,
84
+ } from "@joymerrevent/porters-connect";
85
+
86
+ const porters = new PortersClient({
87
+ host: "sandbox.invalid",
88
+ appId: "demo",
89
+ appSecret: "demo",
90
+ partition: 1,
91
+ transport: createMockTransport((req) =>
92
+ req.url.includes("/v1/candidate")
93
+ ? `<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>`
94
+ : undefined,
95
+ ), // 未モックのリクエストは明示エラー(フェイルセーフ)
96
+ });
97
+
98
+ const page = await porters.candidate.search();
99
+ console.log(page.items[0]?.P_Name); // 山田 太郎
100
+ ```
101
+
102
+ そのまま動くサンプルも同梱しています([`examples/offline-sandbox.ts`][sandbox])。
103
+
104
+ ```sh
105
+ pnpm sandbox
106
+ ```
107
+
108
+ 実利用では `transport` を渡さず、`host` / `appId` / `appSecret` を設定するだけです。
109
+
110
+ ## 認証
111
+
112
+ `appId` / `appSecret` を渡すと、ライブラリが**透過的に** `code_direct` でトークンを取得・キャッシュし、
113
+ 失効時に自動更新します(`connect()` の明示呼び出しは不要)。トークンの保存先は既定でインメモリ。
114
+ 複数インスタンス運用では `tokenStore` を注入して Redis / DB / ファイルに永続化できます。
115
+
116
+ ```ts
117
+ import type { TokenStore } from "@joymerrevent/porters-connect";
118
+
119
+ const tokenStore: TokenStore = {
120
+ get: async () => /* StoredTokens | undefined */ undefined,
121
+ set: async (t) => {
122
+ /* 保存 */
123
+ },
124
+ clear: async () => {
125
+ /* 破棄 */
126
+ },
127
+ };
128
+ new PortersClient({ host, appId, appSecret, partition, tokenStore });
129
+ ```
130
+
131
+ `transport`(HTTP 注入)や `auth`(独自 `TokenProvider`)も差し替え可能です。
132
+
133
+ ## リソースと操作
134
+
135
+ すべてのデータ系リソースは同じ形のアクセサを持ちます。
136
+
137
+ | アクセサ | リソース | メソッド |
138
+ | -------------------- | ------------ | ---------------------------------------------------- |
139
+ | `porters.candidate` | 個人連絡先 | `search` / `searchAll` / `get` / `create` / `update` |
140
+ | `porters.job` | JOB | `search` / `searchAll` / `get` / `create` / `update` |
141
+ | `porters.client` | 企業 | `search` / `searchAll` / `get` / `create` / `update` |
142
+ | `porters.process` | 選考プロセス | `search` / `searchAll` / `get` / `create` / `update` |
143
+ | `porters.resume` | レジュメ | `search` / `searchAll` / `get` / `create` / `update` |
144
+ | `porters.attachment` | 添付ファイル | `search` / `get` / `create` / `update` |
145
+
146
+ - `search(query?)` → `{ items, total, count, start }`(オフセット式ページング)。
147
+ - `searchAll(query?)` → `AsyncIterable`(200 件刻みで全件 yield)。
148
+ - `get(id)` → 1 件 or `undefined`。
149
+ - `create(input)` → 採番された **id(number)**。
150
+ - `update(id, input)` → その **id**。
151
+
152
+ **検索クエリ**(`query`)の主なキー:
153
+
154
+ - `field`:取得する項目(接頭辞付き alias の配列。例 `["Person.P_Id", "Person.P_Name"]`)。
155
+ **省略するとカタログ上の全項目を既定取得**します(PORTERS は field 未指定だと主キーのみ返すため、
156
+ ライブラリが既定 field を補います)。`field: []`(空配列)を渡すと API 仕様どおり**主キーのみ**を返します(件数取得など)。
157
+ - `condition`:検索条件。`{ "[Alias]:[suffix]": "値" }` 形式。`suffix` は型ごとに
158
+ `eq`/`gt`/`ge`/`le`/`lt`(数値・日時)、`part`/`full`(テキスト)、`or`/`and`(Option)。
159
+ - `count`(1–200・既定 10)、`start`(0 始まり)。
160
+
161
+ > **削除 API はありません**(PORTERS 仕様)。`delete()` メソッドは提供しません。削除済みは検索の状態フィルタで扱います。
162
+
163
+ ### マスタ Read(読み取り専用)
164
+
165
+ マスタ系は**読み取り専用**で、実 API が受けるクエリだけを持ちます(`condition` と `get(id)` はありません)。
166
+
167
+ | アクセサ | リソース | メソッド | 主なクエリ |
168
+ | ------------------- | ----------------- | ---------------------------------- | ----------------------------------------- |
169
+ | `porters.partition` | Partition | `search` / `searchAll` | `requestType`(1=アクセス可能一覧・既定) |
170
+ | `porters.user` | User | `search` / `searchAll` / `current` | `requestType` / `userType` / `field` |
171
+ | `porters.field` | Field(項目定義) | `search` / `searchAll` | `resource`(必須)/ `active` |
172
+ | `porters.option` | Option(選択肢) | `search` | `alias` / `level` / `enabled` |
173
+
174
+ ```ts
175
+ // アクセス可能な Partition(Company DB)を発見
176
+ const partitions = await porters.partition.search();
177
+
178
+ // 現在の API ユーザー(code_direct ではアプリ自身の User)=自己同定
179
+ const me = await porters.user.current();
180
+
181
+ // Job リソースの項目定義(U_/A_ カスタム項目を含む)を取得
182
+ const fields = await porters.field.search({ resource: "job" });
183
+
184
+ // 選択肢マスタをフラットな配列で取得(親子は P_ParentId で復元)
185
+ const options = await porters.option.search({ alias: "Option.P_Gender" });
186
+ ```
187
+
188
+ - `porters.option.search()` は入れ子の選択肢ツリーを**深さ優先でフラット化**して返します(全ノード・`P_ParentId`/`P_Order` で階層復元可)。`start` が無いため `searchAll` はありません。
189
+ - `porters.partition.current()` は提供しません(`request_type=0` は既定の `code_direct` 認証では 403 になるため)。一覧は `search()`(既定 `requestType: 1`)で取得します。
190
+
191
+ ## 読み取り値の表現
192
+
193
+ `search` / `get` の各レコードは、項目 alias(接頭辞無し)をキーにした型付きオブジェクトです。
194
+ PORTERS の Field Type に応じてデコードされます。
195
+
196
+ | Field Type | 返り値 |
197
+ | --------------------------------------------------- | ----------------------------------------------- |
198
+ | Id / Number / Currency | `number` |
199
+ | 文字列系(Singleline/Multiline/Mail/Telephone/URL) | `string` |
200
+ | DateTime | ISO 8601 `...Z`(UTC) |
201
+ | Date / Age | `yyyy-mm-dd` |
202
+ | Option(単一・複数とも) | **`string[]`**(選択 alias の配列) |
203
+ | User | `{ P_Id, P_Type, P_Name, P_Mail }`(`UserRef`) |
204
+ | System[Reference](関連 Client/Job 等) | 参照先の **id(number)** |
205
+ | 空・未設定 | `null` |
206
+
207
+ ```ts
208
+ const c = await porters.candidate.get(10001);
209
+ c?.P_Id; // number
210
+ c?.P_Name; // string | null
211
+ c?.P_RegistrationDate; // "2026-01-02T03:04:05Z" | null
212
+ c?.P_Phase; // string[] | null(例 ["Option.P_PersonPhase_Applied"])
213
+ c?.P_Owner; // { P_Id, P_Name, ... } | null
214
+ ```
215
+
216
+ ## 書き込み(create / update)
217
+
218
+ 入力も項目 alias(接頭辞無し)をキーにしたオブジェクトです。値の渡し方:
219
+
220
+ - **User / Reference 項目**:関連レコードの **id(number)**。
221
+ - **Option 項目**:選択 alias の **配列(`string[]`)**(単一選択も 1 要素配列)。
222
+ - **日時**:ISO 8601(`...Z`)。ライブラリが PORTERS 形式へ変換します。
223
+ - `null`:その項目を**省略**(不変)。`""`:文字列項目をクリア。
224
+ - `P_Id` は `create`/`update` が自動で付与するため**指定不要**(型でも受け付けません)。
225
+ - 入力は項目ごとに**静的型付き**です(Option は `string[]`、User/参照は `number`…)。`create` は
226
+ リソースが新規登録で要求する項目を**型で必須化**します(例: `P_Owner`、Process は関連 6 項目)。
227
+ `update` は全項目任意です。登録日/更新日などの読み取り専用項目は型に出ません。
228
+
229
+ ```ts
230
+ // 作成(新規)
231
+ const newId = await porters.candidate.create({
232
+ P_Owner: 5, // User 項目は id
233
+ P_Name: "鈴木 一郎",
234
+ P_Reading: "すずき いちろう",
235
+ });
236
+
237
+ // 更新
238
+ await porters.candidate.update(newId, { P_Mail: "ichiro@example.com" });
239
+
240
+ // 選考プロセス(関連 id を指定)
241
+ await porters.process.create({
242
+ P_Owner: 1,
243
+ P_Client: 100,
244
+ P_Recruiter: 200,
245
+ P_Job: 300,
246
+ P_Candidate: 10001,
247
+ P_Resume: 50,
248
+ });
249
+ ```
250
+
251
+ > 1 リクエストの長さは**約 15000 文字**まで(PORTERS 仕様)。超えるとライブラリが送信前に弾きます。
252
+
253
+ ## 添付ファイル(Attachment)
254
+
255
+ `Content` は Base64 文字列です。バイト列からの変換ヘルパー(opt-in)も同梱しています。
256
+
257
+ ```ts
258
+ import { bytesToBase64 } from "@joymerrevent/porters-connect";
259
+
260
+ const id = await porters.attachment.create({
261
+ resource: 17, // 関連リソース種別コード(Resource List 参照)
262
+ resourceId: 10001, // 関連レコードの id
263
+ contentType: "application/pdf",
264
+ fileName: "履歴書.pdf",
265
+ content: bytesToBase64(fileBytes), // Uint8Array -> Base64
266
+ });
267
+ ```
268
+
269
+ - 1 ファイル **10MB まで**。超過分はライブラリが送信前に弾きます。
270
+
271
+ ## エラーハンドリング
272
+
273
+ エラーは判別可能な型で throw されます。基底は `PortersError`、系統別に
274
+ `PortersAuthError` / `PortersResourceError` / `PortersNetworkError` / `PortersConfigError`。
275
+
276
+ ```ts
277
+ import { PortersError } from "@joymerrevent/porters-connect";
278
+
279
+ try {
280
+ await porters.candidate.get(1);
281
+ } catch (e) {
282
+ if (e instanceof PortersError) {
283
+ e.category; // "auth" | "permission" | "notFound" | "conflict" | "network" | ...
284
+ e.code; // PORTERS のコード(無い場合 null)
285
+ e.retryable; // 再試行可否
286
+ e.hint; // 対処のヒント(あれば)
287
+ }
288
+ }
289
+ ```
290
+
291
+ - トークン失効は内側で自動回復します。設定ミスは `PortersConfigError` を早期に throw。
292
+ - 一時エラー・ネットワークは内蔵リトライ。非冪等な `create` はネットワーク不確実時に握り潰さず表面化します。
293
+ - レート制限超過時、PORTERS は判別可能なコードを返さず接続を切るため、`PortersNetworkError`(category `"network"`)として表面化します(`category` の `"rateLimit"` は将来の配線用に予約された値で、現状はどの分類も produce しません)。
294
+
295
+ > 症状別の早見表と 2 系統(認証 / リソース)のコード対応表は [エラーハンドリング ガイド][error-handling]にまとめています。
296
+
297
+ ## PORTERS 固有の注意
298
+
299
+ - **削除 API は存在しない**(`delete()` は提供しない)。
300
+ - **日時は UTC 前提**。ISO 8601(`...Z`)で入出力し、JST 等の変換はしない(利用側の責務)。
301
+ - **レート制限**:1 分あたり Read 2000 / Write 500、月 15 万アクセス。内蔵スロットリングで分散。
302
+ - **ホスト名は非公開**:`PORTERS_HOST` で受け取り、ハードコードしない。
303
+
304
+ ## 対応バージョン
305
+
306
+ - **Connect API Version 2**(ヘッダ `X-P-ConnectAPI-Version: 2` を既定送信)。
307
+ - PORTERS 8.x / 9.x を想定。**正典は [docs/reference][ref]**(実 API ドキュメントに接地)。
308
+
309
+ ## リンク
310
+
311
+ - 設計・決定の記録:[ADR][adr] / 基本設計:[docs/design][design] / API 事実:[docs/reference][ref]
312
+ - 提供元:[Joymerrevent][joymerrevent]
313
+
314
+ ## ライセンス
315
+
316
+ [MIT][mit] © Joymerrevent
317
+
318
+ [mit]: ./LICENSE
319
+ [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg
320
+ [node-badge]: https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg
321
+ [joymerrevent]: https://github.com/Joymerrevent
322
+ [auth-flow]: ./docs/reference/authentication-api/README.md
323
+ [error-handling]: ./docs/guide/error-handling.md
324
+ [sandbox]: ./examples/offline-sandbox.ts
325
+ [adr]: ./docs/adr/README.md
326
+ [design]: ./docs/design/basic-design.md
327
+ [ref]: ./docs/reference/README.md