@joymerrevent/porters-connect 0.4.0 → 0.6.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 +27 -1
- package/README.md +17 -2
- package/dist/index.d.ts +70 -2
- package/dist/index.js +137 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-07-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **一括書き込み**(F-4 / ADR-0041・`CLAUDE.md`)。各データ系リソースに **`createMany`** / **`updateMany`** を追加。
|
|
13
|
+
- `porters.candidate.createMany([...])` / `updateMany([{ id, fields }, ...])` で複数レコードをまとめて書き込み。
|
|
14
|
+
- **1 リクエスト最大 200 件+約 15000 文字**に自動分割(バッチを逐次送信)。1 件で上限超過は送信前に `PortersConfigError`。
|
|
15
|
+
- **非アトミック**: 戻り値 **`BulkWriteResult`**(`results`=送信順の per-item `{ index, id, code, ok }`/`failed`/`hasFailures`)で
|
|
16
|
+
部分成功を返します(per-item の失敗は throw しません)。リクエスト全体の失敗のみ throw(バッチ途中の失敗は既書き込み件数を hint に付与)。
|
|
17
|
+
- `createMany` はバッチ跨ぎで**非冪等**(全体の再実行は作成を重複させ得る)。Attachment は対象外(単件のみ)。
|
|
18
|
+
- 新規 export 型 `BulkWriteResult` / `BulkWriteResultItem`。
|
|
19
|
+
|
|
20
|
+
## [0.5.0] - 2026-06-29
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **マルチテナント面**(F-3 / ADR-0040・ADR-0008/0021)。`porters.tenant(id)` で partition(Company DB)を
|
|
25
|
+
束ねたアクセサ群 `TenantScope` を返します。`porters.tenant(123).candidate.search(...)` は `partition=123`
|
|
26
|
+
を毎回付与し、未束ねの呼び出しは **client 既定 `partition`** を使います(解決は tenant スコープ/client 既定の 2 層)。
|
|
27
|
+
- スコープは data(candidate/job/client/process/resume)+ attachment + master Read(user/field/option)を露出。
|
|
28
|
+
`auth`(App 単位)・`partition` マスタ(発見専用・partition 非送信)・`tenant`(ネスト不可)は含みません。
|
|
29
|
+
- partition 別トークンが必要な場合は**テナント別 `PortersClient`** を構築(ADR-0008 案3)。
|
|
30
|
+
- 新規 export 型 `TenantScope`。`PortersClientOptions.partition` の JSDoc を更新(per-call 引数は設けない方針)。
|
|
31
|
+
|
|
8
32
|
## [0.4.0] - 2026-06-27
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -100,7 +124,9 @@
|
|
|
100
124
|
[oauth-guide]: docs/guide/oauth.md
|
|
101
125
|
[kac]: https://keepachangelog.com/en/1.1.0/
|
|
102
126
|
[semver]: https://semver.org/
|
|
103
|
-
[unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.
|
|
127
|
+
[unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.6.0...HEAD
|
|
128
|
+
[0.6.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.5.0...v0.6.0
|
|
129
|
+
[0.5.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.4.0...v0.5.0
|
|
104
130
|
[0.4.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.3.0...v0.4.0
|
|
105
131
|
[0.3.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.2.1...v0.3.0
|
|
106
132
|
[0.2.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -56,9 +56,13 @@ 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
|
+
// 複数 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
|
+
|
|
62
66
|
// 検索(最大 200 件/ページ)。condition は項目の Data Type ごとに型付き
|
|
63
67
|
const page = await porters.candidate.search({
|
|
64
68
|
condition: { P_Name: { part: "山田" } }, // テキストは part(部分一致)/ full(完全一致)
|
|
@@ -79,6 +83,8 @@ for await (const c of porters.candidate.searchAll({
|
|
|
79
83
|
}
|
|
80
84
|
```
|
|
81
85
|
|
|
86
|
+
> 複数 partition を 1 つの App で扱う(マルチテナント SaaS)場合の `porters.tenant(id)` スコープ・テナント別 client・partition 発見は [マルチテナント ガイド][multi-tenancy] にまとめています。
|
|
87
|
+
>
|
|
82
88
|
> 認証情報やホスト名は**コミットしない**でください。`.env.example` を参考に `.env` で渡します。
|
|
83
89
|
|
|
84
90
|
## 契約なしで試す(オフライン評価)
|
|
@@ -275,9 +281,16 @@ await porters.process.create({
|
|
|
275
281
|
P_Candidate: 10001,
|
|
276
282
|
P_Resume: 50,
|
|
277
283
|
});
|
|
284
|
+
|
|
285
|
+
// 一括作成/更新(200 件+サイズで自動分割・部分成功を返す)
|
|
286
|
+
const bulk = await porters.candidate.createMany([
|
|
287
|
+
{ P_Owner: 5, P_Name: "山田 太郎" },
|
|
288
|
+
{ P_Owner: 5, P_Name: "鈴木 花子" },
|
|
289
|
+
]);
|
|
290
|
+
if (bulk.hasFailures) console.warn(bulk.failed); // per-item は throw せず結果で返す
|
|
278
291
|
```
|
|
279
292
|
|
|
280
|
-
> 1 リクエストの長さは**約 15000 文字**まで(PORTERS
|
|
293
|
+
> 1 リクエストの長さは**約 15000 文字**まで(PORTERS 仕様)。超えるとライブラリが送信前に弾きます。一括書き込み(`createMany` / `updateMany`)は 200 件+サイズで自動分割し、部分成功を `BulkWriteResult` で返します — 詳しくは [一括書き込み ガイド][bulk-write] を参照。
|
|
281
294
|
|
|
282
295
|
## 添付ファイル(Attachment)
|
|
283
296
|
|
|
@@ -365,6 +378,8 @@ try {
|
|
|
365
378
|
[auth-flow]: ./docs/reference/authentication-api/README.md
|
|
366
379
|
[oauth-guide]: ./docs/guide/oauth.md
|
|
367
380
|
[error-handling]: ./docs/guide/error-handling.md
|
|
381
|
+
[multi-tenancy]: ./docs/guide/multi-tenancy.md
|
|
382
|
+
[bulk-write]: ./docs/guide/bulk-write.md
|
|
368
383
|
[sandbox]: ./examples/offline-sandbox.ts
|
|
369
384
|
[adr]: ./docs/adr/README.md
|
|
370
385
|
[design]: ./docs/design/basic-design.md
|
package/dist/index.d.ts
CHANGED
|
@@ -252,6 +252,28 @@ type SearchQuery<F extends FieldCatalog = FieldCatalog> = {
|
|
|
252
252
|
start?: number;
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
/** One record's outcome from a bulk write, in the position it was sent (ADR-0041 SD-2). */
|
|
256
|
+
type BulkWriteResultItem = {
|
|
257
|
+
/** 0-based index in the input array. */
|
|
258
|
+
index: number;
|
|
259
|
+
/** Assigned (create) / echoed (update) record id. Meaningful only when `ok`. */
|
|
260
|
+
id: number;
|
|
261
|
+
/** PORTERS per-item Result Code (`0` = success). */
|
|
262
|
+
code: number;
|
|
263
|
+
/** `code === 0`. */
|
|
264
|
+
ok: boolean;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The result of `createMany` / `updateMany` (ADR-0041). `results` holds every record's outcome in
|
|
268
|
+
* input order; `failed` is the `ok === false` subset. A per-item `code !== 0` does **not** throw —
|
|
269
|
+
* a bulk write mixes successes and failures — so always inspect `hasFailures` / `failed`.
|
|
270
|
+
*/
|
|
271
|
+
type BulkWriteResult = {
|
|
272
|
+
results: BulkWriteResultItem[];
|
|
273
|
+
failed: BulkWriteResultItem[];
|
|
274
|
+
hasFailures: boolean;
|
|
275
|
+
};
|
|
276
|
+
|
|
255
277
|
type WritableKeys<F extends FieldCatalog> = {
|
|
256
278
|
[K in keyof F]: F[K] extends WritableDataType ? K : never;
|
|
257
279
|
}[keyof F];
|
|
@@ -277,6 +299,22 @@ type Resource<F extends FieldCatalog, Req extends keyof F> = {
|
|
|
277
299
|
create(input: CreateInput<F, Req>): Promise<number>;
|
|
278
300
|
/** Update one record by id; resolves to that id. */
|
|
279
301
|
update(id: number, input: UpdateInput<F>): Promise<number>;
|
|
302
|
+
/**
|
|
303
|
+
* Create many records in one call (ADR-0041 / F-4). Auto-batched to ≤200 records and under the
|
|
304
|
+
* request size cap. **Not atomic** — inspect the {@link BulkWriteResult}: per-record failures are
|
|
305
|
+
* returned (`failed` / `hasFailures`), not thrown. Only a whole-request failure throws (with the
|
|
306
|
+
* already-written count). Batching is non-idempotent: a full retry after a mid-run failure may
|
|
307
|
+
* duplicate creates. Empty input sends no request.
|
|
308
|
+
*/
|
|
309
|
+
createMany(inputs: CreateInput<F, Req>[]): Promise<BulkWriteResult>;
|
|
310
|
+
/**
|
|
311
|
+
* Update many records by id in one call (ADR-0041 / F-4). Auto-batched like {@link createMany};
|
|
312
|
+
* per-record failures are returned in the {@link BulkWriteResult}, not thrown.
|
|
313
|
+
*/
|
|
314
|
+
updateMany(items: {
|
|
315
|
+
id: number;
|
|
316
|
+
fields: UpdateInput<F>;
|
|
317
|
+
}[]): Promise<BulkWriteResult>;
|
|
280
318
|
};
|
|
281
319
|
|
|
282
320
|
declare const FIELDS$8: {
|
|
@@ -714,7 +752,11 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
|
|
|
714
752
|
appId?: string;
|
|
715
753
|
appSecret?: string;
|
|
716
754
|
scopes?: Scope[];
|
|
717
|
-
/**
|
|
755
|
+
/**
|
|
756
|
+
* Default partition (Company DB) for every call. For multi-tenant routing, bind a partition
|
|
757
|
+
* per call with {@link PortersClient.tenant} (ADR-0040 / F-3); for a fully separated per-partition
|
|
758
|
+
* token, construct a dedicated client per tenant instead (ADR-0008 案3).
|
|
759
|
+
*/
|
|
718
760
|
partition?: PartitionId;
|
|
719
761
|
/** Custom auth strategy; defaults to the transparent code_direct strategy. */
|
|
720
762
|
auth?: TokenProvider;
|
|
@@ -729,6 +771,22 @@ type PortersClientOptions<C extends DeclaredCatalogs = EmptyCatalog> = {
|
|
|
729
771
|
*/
|
|
730
772
|
fields?: DefinedFields<C>;
|
|
731
773
|
};
|
|
774
|
+
/**
|
|
775
|
+
* The partition-bound resource accessors returned by {@link PortersClient.tenant} (ADR-0040 / F-3).
|
|
776
|
+
* Every accessor here routes to the bound tenant's partition. `auth` (App-level), the `partition`
|
|
777
|
+
* master (discovery — partition-less), and `tenant` itself (no nesting) are deliberately absent.
|
|
778
|
+
*/
|
|
779
|
+
type TenantScope<C extends DeclaredCatalogs = EmptyCatalog> = {
|
|
780
|
+
readonly candidate: CandidateResource<CustomFor<C, "candidate">>;
|
|
781
|
+
readonly job: JobResource<CustomFor<C, "job">>;
|
|
782
|
+
readonly client: ClientResource<CustomFor<C, "client">>;
|
|
783
|
+
readonly process: ProcessResource<CustomFor<C, "process">>;
|
|
784
|
+
readonly resume: ResumeResource<CustomFor<C, "resume">>;
|
|
785
|
+
readonly attachment: AttachmentResource;
|
|
786
|
+
readonly user: UserResource;
|
|
787
|
+
readonly field: FieldResource;
|
|
788
|
+
readonly option: OptionResource;
|
|
789
|
+
};
|
|
732
790
|
/**
|
|
733
791
|
* Entry point of the library. Wires the default transport / auth / throttle /
|
|
734
792
|
* requester and exposes namespaced resource accessors such as `candidate`
|
|
@@ -752,6 +810,16 @@ declare class PortersClient<C extends DeclaredCatalogs = EmptyCatalog> {
|
|
|
752
810
|
readonly field: FieldResource;
|
|
753
811
|
/** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
|
|
754
812
|
readonly option: OptionResource;
|
|
813
|
+
/**
|
|
814
|
+
* Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
|
|
815
|
+
* scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
|
|
816
|
+
* `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
|
|
817
|
+
* the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
|
|
818
|
+
* User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
|
|
819
|
+
* and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
|
|
820
|
+
* token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
|
|
821
|
+
*/
|
|
822
|
+
readonly tenant: (id: PartitionId) => TenantScope<C>;
|
|
755
823
|
constructor(options: PortersClientOptions<C>);
|
|
756
824
|
/** The configured API host. */
|
|
757
825
|
get host(): string;
|
|
@@ -805,4 +873,4 @@ declare const bytesToBase64: (bytes: Uint8Array) => string;
|
|
|
805
873
|
/** Decode a Base64 string back to raw bytes. */
|
|
806
874
|
declare const base64ToBytes: (b64: string) => Uint8Array;
|
|
807
875
|
|
|
808
|
-
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 TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, base64ToBytes, bytesToBase64, createMockTransport, defineFields };
|
|
876
|
+
export { type Attachment, type AttachmentCreate, type AttachmentPage, type AttachmentResource, type AttachmentSearchQuery, type AttachmentUpdate, type AuthApi, type AuthorizationUrlOptions, type BulkWriteResult, type BulkWriteResultItem, type Candidate, type CandidateCreateInput, type CandidatePage, type CandidateResource, type CandidateSearchQuery, type CandidateUpdateInput, type Client, type ClientCreateInput, type ClientPage, type ClientResource, type ClientSearchQuery, type ClientUpdateInput, type Condition, type CustomDataType, type DefinedFields, type ErrorCategory, type Field, type FieldBuilder, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldValue, type GetAccessTokenOptions, type ItemState, type Job, type JobCreateInput, type JobPage, type JobResource, type JobSearchQuery, type JobUpdateInput, type MockHandler, type MockReply, type MockTransportOptions, type Option, type OptionResource, type OptionSearchQuery, type Order, type Partition, type PartitionId, type PartitionPage, type PartitionResource, type PartitionSearchQuery, PortersAuthError, PortersClient, type PortersClientOptions, PortersConfigError, PortersError, type PortersErrorContext, type PortersErrorOptions, PortersNetworkError, PortersResourceError, type Process, type ProcessCreateInput, type ProcessPage, type ProcessResource, type ProcessSearchQuery, type ProcessUpdateInput, type ResourceType, type Resume, type ResumeCreateInput, type ResumePage, type ResumeResource, type ResumeSearchQuery, type ResumeUpdateInput, type RevokeUrlOptions, type 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
|
@@ -590,10 +590,9 @@ var encodeItem = (prefix, fields, item) => {
|
|
|
590
590
|
}
|
|
591
591
|
return parts.join("");
|
|
592
592
|
};
|
|
593
|
+
var encodeWriteItem = (prefix, fields, item) => `<Item>${encodeItem(prefix, fields, item)}</Item>`;
|
|
593
594
|
var buildWriteXml = (config) => {
|
|
594
|
-
const items = config.items.map(
|
|
595
|
-
(item) => `<Item>${encodeItem(config.prefix, config.fields, item)}</Item>`
|
|
596
|
-
).join("");
|
|
595
|
+
const items = config.items.map((item) => encodeWriteItem(config.prefix, config.fields, item)).join("");
|
|
597
596
|
return `<${config.resource}>${items}</${config.resource}>`;
|
|
598
597
|
};
|
|
599
598
|
|
|
@@ -780,6 +779,87 @@ var appendReadQuery = (p, q, ctx) => {
|
|
|
780
779
|
}
|
|
781
780
|
};
|
|
782
781
|
|
|
782
|
+
// src/resources/bulk-write.ts
|
|
783
|
+
var MAX_ITEMS_PER_REQUEST = 200;
|
|
784
|
+
var packBatches = (encoded, budget) => {
|
|
785
|
+
const batches = [];
|
|
786
|
+
let batch = [];
|
|
787
|
+
let length = 0;
|
|
788
|
+
for (const e of encoded) {
|
|
789
|
+
if (e.xml.length > budget) {
|
|
790
|
+
throw new PortersConfigError(
|
|
791
|
+
`a single record serializes to ${e.xml.length} characters, over the ~${budget}-character budget for one request`,
|
|
792
|
+
{
|
|
793
|
+
category: "config",
|
|
794
|
+
hint: "Reduce that record's field values \u2014 a bulk request (URL + body) is capped at ~15000 characters."
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (batch.length >= MAX_ITEMS_PER_REQUEST || length + e.xml.length > budget) {
|
|
799
|
+
batches.push(batch);
|
|
800
|
+
batch = [];
|
|
801
|
+
length = 0;
|
|
802
|
+
}
|
|
803
|
+
batch.push(e);
|
|
804
|
+
length += e.xml.length;
|
|
805
|
+
}
|
|
806
|
+
if (batch.length > 0) batches.push(batch);
|
|
807
|
+
return batches;
|
|
808
|
+
};
|
|
809
|
+
var batchFailure = (cause, written, resource, partition) => {
|
|
810
|
+
const base = cause instanceof PortersError ? cause : void 0;
|
|
811
|
+
return new PortersResourceError(
|
|
812
|
+
`bulk write failed after ${written} record(s) had already been written`,
|
|
813
|
+
{
|
|
814
|
+
category: base?.category ?? "unknown",
|
|
815
|
+
code: base?.code ?? null,
|
|
816
|
+
retryable: false,
|
|
817
|
+
httpStatus: base?.httpStatus,
|
|
818
|
+
hint: `${written} record(s) from earlier batches were already written; retry only the records from index ${written} onward (create is non-idempotent \u2014 a full retry would duplicate them).`,
|
|
819
|
+
context: { resource, operation: "bulkWrite", partition },
|
|
820
|
+
cause
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
};
|
|
824
|
+
var runBulkWrite = async (requester, target, items, idempotent) => {
|
|
825
|
+
const envelope = `<${target.name}></${target.name}>`.length;
|
|
826
|
+
const budget = MAX_REQUEST_LENGTH - target.url.length - envelope;
|
|
827
|
+
const encoded = items.map((item, index) => ({
|
|
828
|
+
index,
|
|
829
|
+
xml: encodeWriteItem(target.prefix, target.fields, item)
|
|
830
|
+
}));
|
|
831
|
+
const batches = packBatches(encoded, budget);
|
|
832
|
+
const results = [];
|
|
833
|
+
let written = 0;
|
|
834
|
+
for (const batch of batches) {
|
|
835
|
+
const body = `<${target.name}>${batch.map((e) => e.xml).join("")}</${target.name}>`;
|
|
836
|
+
let parsed;
|
|
837
|
+
try {
|
|
838
|
+
parsed = await requester.request(
|
|
839
|
+
{ method: "POST", url: target.url, headers: {}, body },
|
|
840
|
+
parseWriteResult,
|
|
841
|
+
{ write: true, idempotent }
|
|
842
|
+
);
|
|
843
|
+
} catch (cause) {
|
|
844
|
+
if (written === 0) throw cause;
|
|
845
|
+
throw batchFailure(cause, written, target.name, target.partition);
|
|
846
|
+
}
|
|
847
|
+
if (parsed.length !== batch.length) {
|
|
848
|
+
throw new PortersResourceError(
|
|
849
|
+
`bulk write response returned ${parsed.length} result(s) for ${batch.length} record(s)`,
|
|
850
|
+
{ category: "unknown", context: { resource: target.name } }
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
batch.forEach((e, i) => {
|
|
854
|
+
const { id, code } = parsed[i];
|
|
855
|
+
results.push({ index: e.index, id, code, ok: code === 0 });
|
|
856
|
+
});
|
|
857
|
+
written += batch.length;
|
|
858
|
+
}
|
|
859
|
+
const failed = results.filter((r) => !r.ok);
|
|
860
|
+
return { results, failed, hasFailures: failed.length > 0 };
|
|
861
|
+
};
|
|
862
|
+
|
|
783
863
|
// src/resources/resource.ts
|
|
784
864
|
var USER_SUBFIELDS = ["P_Id", "P_Type", "P_Name", "P_Mail"];
|
|
785
865
|
var defaultFieldList = (prefix, fields) => Object.entries(fields).map(
|
|
@@ -851,7 +931,25 @@ var createResource = (config, deps) => {
|
|
|
851
931
|
);
|
|
852
932
|
const create = (input) => write({ ...input, P_Id: -1 }, false);
|
|
853
933
|
const update = (id, input) => write({ ...input, P_Id: id }, true);
|
|
854
|
-
|
|
934
|
+
const target = {
|
|
935
|
+
name: config.name,
|
|
936
|
+
prefix: config.prefix,
|
|
937
|
+
fields: fieldMap,
|
|
938
|
+
partition: deps.partition
|
|
939
|
+
};
|
|
940
|
+
const createMany = (inputs) => runBulkWrite(
|
|
941
|
+
deps.requester,
|
|
942
|
+
{ ...target, url: writeUrl() },
|
|
943
|
+
inputs.map((input) => ({ ...input, P_Id: -1 })),
|
|
944
|
+
false
|
|
945
|
+
);
|
|
946
|
+
const updateMany = (items) => runBulkWrite(
|
|
947
|
+
deps.requester,
|
|
948
|
+
{ ...target, url: writeUrl() },
|
|
949
|
+
items.map(({ id, fields }) => ({ ...fields, P_Id: id })),
|
|
950
|
+
true
|
|
951
|
+
);
|
|
952
|
+
return { search, searchAll, get, create, update, createMany, updateMany };
|
|
855
953
|
};
|
|
856
954
|
|
|
857
955
|
// src/resources/candidate.ts
|
|
@@ -1339,6 +1437,16 @@ var PortersClient = class {
|
|
|
1339
1437
|
field;
|
|
1340
1438
|
/** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
|
|
1341
1439
|
option;
|
|
1440
|
+
/**
|
|
1441
|
+
* Bind a tenant's partition (Company DB) once and route every call through it — the multi-tenant
|
|
1442
|
+
* scope (ADR-0008 案2 / renamed in ADR-0021 / implemented in ADR-0040 F-3).
|
|
1443
|
+
* `porters.tenant(123).candidate.search(...)` sends `partition=123` without repeating it, overriding
|
|
1444
|
+
* the client-default `partition`. Returns the partition-bound accessors (data + attachment + master
|
|
1445
|
+
* User/Field/Option); `auth` (App-level), the `partition` master (discovery — takes no partition),
|
|
1446
|
+
* and `tenant` itself (no nesting) are intentionally omitted. For a fully separated per-partition
|
|
1447
|
+
* token, construct a dedicated {@link PortersClient} per tenant instead (ADR-0008 案3).
|
|
1448
|
+
*/
|
|
1449
|
+
tenant;
|
|
1342
1450
|
#host;
|
|
1343
1451
|
constructor(options) {
|
|
1344
1452
|
const transport = options.transport ?? createFetchTransport();
|
|
@@ -1373,22 +1481,33 @@ var PortersClient = class {
|
|
|
1373
1481
|
backoff: expoBackoff()
|
|
1374
1482
|
});
|
|
1375
1483
|
this.#host = options.host;
|
|
1376
|
-
const deps = {
|
|
1377
|
-
requester,
|
|
1378
|
-
host: options.host,
|
|
1379
|
-
partition: options.partition ?? 0
|
|
1380
|
-
};
|
|
1381
1484
|
const customFor = (key) => options.fields?.[key] ?? {};
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1485
|
+
const buildScope = (partition) => {
|
|
1486
|
+
const deps = { requester, host: options.host, partition };
|
|
1487
|
+
return {
|
|
1488
|
+
candidate: createCandidateResource(deps, customFor("candidate")),
|
|
1489
|
+
job: createJobResource(deps, customFor("job")),
|
|
1490
|
+
client: createClientResource(deps, customFor("client")),
|
|
1491
|
+
process: createProcessResource(deps, customFor("process")),
|
|
1492
|
+
resume: createResumeResource(deps, customFor("resume")),
|
|
1493
|
+
attachment: createAttachmentResource(deps),
|
|
1494
|
+
user: createUserResource(deps),
|
|
1495
|
+
field: createFieldResource(deps),
|
|
1496
|
+
option: createOptionResource(deps)
|
|
1497
|
+
};
|
|
1498
|
+
};
|
|
1499
|
+
this.tenant = buildScope;
|
|
1500
|
+
const root = buildScope(options.partition ?? 0);
|
|
1501
|
+
this.candidate = root.candidate;
|
|
1502
|
+
this.job = root.job;
|
|
1503
|
+
this.client = root.client;
|
|
1504
|
+
this.process = root.process;
|
|
1505
|
+
this.resume = root.resume;
|
|
1506
|
+
this.attachment = root.attachment;
|
|
1507
|
+
this.user = root.user;
|
|
1508
|
+
this.field = root.field;
|
|
1509
|
+
this.option = root.option;
|
|
1388
1510
|
this.partition = createPartitionResource({ requester, host: options.host });
|
|
1389
|
-
this.user = createUserResource(deps);
|
|
1390
|
-
this.field = createFieldResource(deps);
|
|
1391
|
-
this.option = createOptionResource(deps);
|
|
1392
1511
|
}
|
|
1393
1512
|
/** The configured API host. */
|
|
1394
1513
|
get host() {
|