@joymerrevent/porters-connect 0.15.0 → 0.16.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,96 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.16.0] - 2026-09-15
9
+
10
+ **カスタム項目を「宣言してから使う」に揃えた版**です。**破壊的変更**(`field` が未宣言の
11
+ `U_` / `A_` を受け付けなくなります)を含みます。逃げ道として `rawValue` を用意しました。
12
+
13
+ ### Added
14
+
15
+ - **`rawValue`** — 宣言していない項目の値を、レコードから**そのまま**読み出します([ADR-0074][adr74])。
16
+
17
+ ```ts
18
+ import { rawValue } from "@joymerrevent/porters-connect";
19
+ import type { CandidateSearchQuery } from "@joymerrevent/porters-connect";
20
+
21
+ const page = await t.candidate.search({
22
+ field: ["P_Name", "U_memo"] as CandidateSearchQuery["field"],
23
+ });
24
+ const memo = rawValue(page.items[0], "U_memo"); // string | null | undefined
25
+ ```
26
+
27
+ 応答に無ければ `undefined`、スカラでなければ(`Option` / `User` / `Image` の入れ子)`null`、
28
+ あれば生の文字列を返します。**変換はしません**ので、日時は PORTERS の書式
29
+ (`2026/09/10 12:00:00`)のままです。宣言できる項目は宣言してください — こちらは逃げ道です。
30
+
31
+ ### Changed
32
+
33
+ - **(破壊的)`field` が未宣言のカスタム項目を受け付けなくなりました**([ADR-0074][adr74])。
34
+ カスタム項目を使う入口は 4 つ(`field` / `condition` / `order` / 書き込み)あり、**`field` だけが
35
+ 宣言なしの `U_` / `A_` を受けていました**。しかも受け取り側の型には出ないため、
36
+ **要求はできるのに読めない**という非対称が残っていました。
37
+
38
+ ```ts
39
+ await t.candidate.search({ field: ["U_memo"] });
40
+ // ^^^^^^^^ 型エラーになります
41
+ ```
42
+
43
+ 直し方は**宣言**です(`defineFields`)。項目 1 つなら 1 行で済み、テナントの項目からは
44
+ `generateFieldDecls` で生成できます。宣言すると綴りが検査され、値も宣言した Data Type で
45
+ 変換されます。**実行時の挙動は変えていません** — 型を外せば送れますし、応答に知らない項目が
46
+ 混ざっても落ちません。宣言せずに触る必要があるときは、上記の `rawValue` を使ってください。
47
+
48
+ - **(型のみ)宣言の違うクライアントを関数に渡せなくなりました**。`TenantScope<typeof fields>` は
49
+ その宣言のスコープだけを受け取ります。以前は型が通ってしまい、`number` と型が言う値に生の
50
+ 文字列が入ることがありました。どの宣言でも受ける関数は `TenantScope<DeclaredCatalogs>` と
51
+ 書けます(これまでどおり)。
52
+
53
+ - **使い方ドキュメントで、読み取りと書き込みの非対称を揃えました**。`Option` / `User` /
54
+ `System[Reference]` / `Image` / `Link` は Read と Write で形が違うため、同じ項目の往復に
55
+ 読み替えが要ります。ガイドと[正典(PORTERS API の事実)][ref]の双方を、出典に突き合わせて
56
+ 埋めました。パッケージの中身は変わりません。
57
+
58
+ - **`src/fields` が品質ゲートの対象に戻りました**([RV-44][rv44])。カバレッジとミューテーションの
59
+ 除外リストに、プレースホルダだった頃の `src/fields/**` が残っていました。宣言 DSL と
60
+ テナントカタログの照合はすでに実ロジックなので、計測対象に戻し、見つかった抜けを埋めています。
61
+ こちらもパッケージの中身は変わりません。
62
+
63
+ ## [0.15.1] - 2026-09-13
64
+
65
+ **開発・CI まわりだけの版**です。**公開されるパッケージの中身(`dist`)は 0.15.0 と同一**で、
66
+ 公開 API・型・挙動に変更はありません(既存コードはそのまま動きます)。
67
+
68
+ ### Security
69
+
70
+ - **開発用依存の既知脆弱性 5 件を解消しました**。いずれもビルド・テスト用ツールの推移依存で、
71
+ **利用者の実行時には入りません**(`dependencies` は `fast-xml-parser` のみ)。
72
+ 依存の解決を `pnpm.overrides` で固定しています。
73
+
74
+ | パッケージ | 経路 | Advisory |
75
+ | ----------------- | --------------------------------------------- | ------------------------------------------------------- |
76
+ | `brace-expansion` | `@stryker-mutator/core` > `minimatch` | [GHSA-mh99-v99m-4gvg][gh1] / [GHSA-rgw5-rvv9-x895][gh2] |
77
+ | `smol-toml` | `markdownlint-cli2` | [GHSA-7w5x-hrqm-74c2][gh5] |
78
+ | `qs` | `@stryker-mutator/core` > `typed-rest-client` | [GHSA-x5fp-wj9c-mxmx][gh3] / [GHSA-4mjr-xmp4-gh2g][gh4] |
79
+
80
+ - **GitHub Actions の書き込み権限を job 単位に絞りました**。タグ作成ワークフローの既定を
81
+ 読み取りのみにし、書き込みは実際に必要な job にだけ与えます。ワークフローが行える操作の範囲は
82
+ 変わりません(最小権限の原則に寄せた整理です)。
83
+
84
+ ### Changed
85
+
86
+ - **往復・エスケープ・分割の不変条件を property-based テストで検査するようになりました**
87
+ ([fast-check][fastcheck])。代表値を 1 点ずつ確かめる形では境界の抜けが見えないため、
88
+ 値を機械に選ばせて不変条件そのものを検査します。対象は 3 つです。
89
+
90
+ - 日時: PORTERS 形式 ⇄ ISO 8601 (UTC) の往復と、オフセット表記によらない UTC 正規化
91
+ - Write XML: エスケープ後に生の `&` `<` `>` が残らないこと、書いて読むと元の値に戻ること
92
+ - bulk write: 200 件と約 15000 文字の 2 つの上限を、どの入力でも同時に満たすこと
93
+
94
+ - **生成 API リファレンスの「Defined in」表記を整理しました**。継承元が依存側にある記号
95
+ (`Error.message` など)のパスから pnpm ストアの版つきディレクトリを落とし、
96
+ `@types/node/globals.d.ts:67` の形にしています。表示されるメンバーは変わりません。
97
+
8
98
  ## [0.15.0] - 2026-09-13
9
99
 
10
100
  **宣言と実物のズレを黙って飲み込まなくなった版**です。**破壊的変更**(読み取りの型不一致が
@@ -702,14 +792,19 @@
702
792
  [adr63]: docs/adr/0063-idempotency-guard-scope.md
703
793
  [rv22]: docs/reviews/rv/0022-ratelimit-create-no-retry.md
704
794
  [rv32]: docs/reviews/rv/0032-searchall-query-mutation.md
795
+ [rv44]: docs/reviews/rv/0044-fields-excluded-from-coverage.md
705
796
  [write-constraints]: docs/usage/concepts/limits.md
706
797
  [adr68]: docs/adr/0068-api-reference-tooling.md
707
798
  [adr69]: docs/adr/0069-tenant-field-catalog-tooling.md
708
799
  [adr73]: docs/adr/0073-throttle-sharing.md
800
+ [adr74]: docs/adr/0074-custom-field-declaration-required.md
709
801
  [lv]: docs/live-verification.md
802
+ [ref]: docs/usage/reference/README.md
710
803
  [kac]: https://keepachangelog.com/en/1.1.0/
711
804
  [semver]: https://semver.org/
712
- [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.14.0...HEAD
805
+ [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.16.0...HEAD
806
+ [0.16.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.15.1...v0.16.0
807
+ [0.15.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.15.0...v0.15.1
713
808
  [0.15.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.14.0...v0.15.0
714
809
  [0.14.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.13.0...v0.14.0
715
810
  [0.13.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.12.1...v0.13.0
@@ -730,3 +825,9 @@
730
825
  [0.2.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.1.1...v0.2.0
731
826
  [0.1.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.1.0...v0.1.1
732
827
  [0.1.0]: https://github.com/Joymerrevent/porters-connect/releases/tag/v0.1.0
828
+ [gh1]: https://github.com/advisories/GHSA-mh99-v99m-4gvg
829
+ [gh2]: https://github.com/advisories/GHSA-rgw5-rvv9-x895
830
+ [gh3]: https://github.com/advisories/GHSA-x5fp-wj9c-mxmx
831
+ [gh4]: https://github.com/advisories/GHSA-4mjr-xmp4-gh2g
832
+ [gh5]: https://github.com/advisories/GHSA-7w5x-hrqm-74c2
833
+ [fastcheck]: https://github.com/dubzzz/fast-check
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @joymerrevent/porters-connect
2
2
 
3
- [![npm version][npm-badge]][npm] [![License: MIT][mit-badge]][mit] ![Node >= 20][node-badge] [![OpenSSF Scorecard][scorecard-badge]][scorecard]
3
+ [![npm version][npm-badge]][npm] [![License: MIT][mit-badge]][mit] ![Node >= 20][node-badge] [![OpenSSF Scorecard][scorecard-badge]][scorecard] [![OpenSSF Best Practices][bp-badge]][bp]
4
4
 
5
5
  PORTERS Connect API(旧 HRBC)を **TypeScript から型安全・簡単に**扱うための、
6
6
  [Joymerrevent(ジョイメリベント)][joymerrevent] 製の **非公式(unofficial)** ラッパーです。
@@ -147,6 +147,8 @@ console.log(page.total, page.items[0]?.P_Name);
147
147
  [node-badge]: https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg
148
148
  [scorecard]: https://scorecard.dev/viewer/?uri=github.com/Joymerrevent/porters-connect
149
149
  [scorecard-badge]: https://api.scorecard.dev/projects/github.com/Joymerrevent/porters-connect/badge
150
+ [bp]: https://www.bestpractices.dev/projects/14611
151
+ [bp-badge]: https://www.bestpractices.dev/projects/14611/badge
150
152
  [joymerrevent]: https://github.com/Joymerrevent
151
153
  [contributing]: ./CONTRIBUTING.md
152
154
  [security]: ./SECURITY.md
package/dist/index.d.ts CHANGED
@@ -268,13 +268,34 @@ type FieldCatalog = Record<string, DataType | null>;
268
268
  type EmptyCatalog = Record<never, never>;
269
269
  /**
270
270
  * A decoded record: every known field, each `DecodedValue | null`, and **optional** because a
271
- * field not named in `field` is simply absent (SD-3 "simple" type — ADR-0005/0019). Custom
272
- * `U_`/`A_` aliases are not in the catalog, so they are not typed here (access via a cast until
273
- * the declaration DSL lands — ADR-0005 SD-2); at runtime they still pass through as raw values.
271
+ * field not named in `field` is simply absent (SD-3 "simple" type — ADR-0005/0019). An alias the
272
+ * catalog does not know is not typed here — declare it with `defineFields` (ADR-0023) to get it
273
+ * typed and converted. At runtime such a field still passes through as a raw value; read it with
274
+ * {@link rawValue} (ADR-0074 D2).
274
275
  */
275
276
  type ReadRecord<F extends FieldCatalog> = {
276
277
  [K in keyof F]?: DecodedValue<F[K]> | null;
277
278
  };
279
+ /**
280
+ * Read a field the catalog does not know (ADR-0074 D2) — the named escape hatch for a value that
281
+ * arrived without a declaration: through a cast in `field`, inside an expanded reference record,
282
+ * or because PORTERS returned a field that was not asked for.
283
+ *
284
+ * Returns what the record actually holds, unconverted:
285
+ *
286
+ * - `undefined` — the alias is not on the record (it was never returned)
287
+ * - `null` — it is there but not a scalar (PORTERS sends a nested node for Option / User / Image)
288
+ * - `string` — the raw text, exactly as PORTERS sent it
289
+ *
290
+ * **No conversion happens.** A date comes back in PORTERS' own format (`2026/09/10 12:00:00`), not
291
+ * ISO 8601, and a number comes back as text. Declare the field with `defineFields` to get the
292
+ * converted, typed value instead — this is the escape hatch, not the normal path.
293
+ *
294
+ * @example
295
+ * const page = await t.candidate.search({ field: ["P_Name"] });
296
+ * const memo = rawValue(page.items[0], "U_memo"); // string | null | undefined
297
+ */
298
+ declare const rawValue: (record: unknown, alias: string) => string | null | undefined;
278
299
  /**
279
300
  * A page of decoded records: the standard Read envelope (Total / Count / Start) around whatever
280
301
  * the item decoder produced. Parametrised by the *record* rather than the catalog because a read
@@ -288,16 +309,21 @@ type ResourcePageOf<T> = {
288
309
  };
289
310
  type ResourcePage<F extends FieldCatalog> = ResourcePageOf<ReadRecord<F>>;
290
311
  /**
291
- * What a Read `field` entry may name (ADR-0059): a catalogued alias — every standard `P_` field
292
- * plus the custom fields declared with `defineFields` (ADR-0023) — or an undeclared tenant custom
293
- * field, admitted by the `U_`/`A_` naming rule `defineFields` already enforces at runtime.
312
+ * What a Read `field` entry may name (ADR-0059 / ADR-0074 D1): a **catalogued** alias — every
313
+ * standard `P_` field plus the custom fields declared with `defineFields` (ADR-0023). An
314
+ * undeclared `U_`/`A_` alias is **not** accepted: `condition`, `order` and the Write inputs have
315
+ * always required a declaration, and ADR-0074 D1 brings `field` in line, so custom fields follow
316
+ * one rule — declare, then use.
294
317
  *
295
318
  * Aliases are **bare**: the resource's prefix (`Person.` for Candidate) is a constant the
296
319
  * descriptor knows, so the library adds it. That makes `condition` / `order` / `field` one
297
320
  * vocabulary and turns a typo (`P_Nmae`) or a hand-written prefix into a compile error instead of
298
321
  * a request that quietly returns nothing.
322
+ *
323
+ * The runtime stays permissive (ADR-0074): an alias that arrives through a cast is still sent, and
324
+ * a response field the catalog does not know still decodes — read it with {@link rawValue}.
299
325
  */
300
- type ReadFieldAlias<F extends FieldCatalog> = (keyof F & string) | `U_${string}` | `A_${string}`;
326
+ type ReadFieldAlias<F extends FieldCatalog> = keyof F & string;
301
327
 
302
328
  /**
303
329
  * The resource a `System[Reference]` field points at, as far as expansion needs it: its alias
@@ -2801,4 +2827,4 @@ declare const bytesToBase64: (bytes: Uint8Array) => string;
2801
2827
  /** Decode a Base64 string back to raw bytes. */
2802
2828
  declare const base64ToBytes: (b64: string) => Uint8Array;
2803
2829
 
2804
- export { type Activity, type ActivityCreateInput, type ActivityPage, type ActivityResource, type ActivitySearchQuery, type ActivityUpdateInput, 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 Contact, type ContactCreateInput, type ContactPage, type ContactResource, type ContactSearchQuery, type ContactUpdateInput, type Contract, type ContractCreateInput, type ContractPage, type ContractResource, type ContractSearchQuery, type ContractUpdateInput, type CustomDataType, type CustomFieldResource, type CustomFor, type DeclaredCatalogs, type DefinedFields, type DepartmentRef, type ErrorCategory, type Expand, type ExpandedReadRecord, type Field, type FieldBuilder, type FieldCatalogSource, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldTypeMismatch, type FieldValue, type FieldVerification, type GenerateFieldDeclsOptions, type GetAccessTokenOptions, type ImageContentType, type ImageOption, type ImageReadRecord, type ImageSelectedValue, type ImageSubField, type ImageValue, type ImageWriteValue, type ItemState, type Job, type JobCreateInput, type JobPage, type JobResource, type JobSearchQuery, type JobUpdateInput, type LinkValue, type MissingField, type MockHandler, type MockReply, type MockTransportOptions, type Opportunity, type OpportunityCreateInput, type OpportunityPage, type OpportunityResource, type OpportunitySearchQuery, type OpportunityUpdateInput, type Option, type OptionResource, type OptionSearchQuery, type Order, type Partition, type PartitionId, type PartitionPage, type PartitionResource, type PartitionSearchQuery, type Phase, type PhaseAccessor, type PhaseCreateInput, type PhasePage, type PhaseResource, type PhaseSearchQuery, type PhaseUpdateInput, 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 ReadCustomCatalogOptions, type ReadFieldAlias, type Recruiter, type RecruiterCreateInput, type RecruiterPage, type RecruiterResource, type RecruiterSearchQuery, type RecruiterUpdateInput, type ReferenceMap, type ReferenceRecord, type ResourceName, type ResourcePageOf, type ResourceType, type Resume, type ResumeCreateInput, type ResumePage, type ResumeResource, type ResumeSearchQuery, type ResumeUpdateInput, type RevokeUrlOptions, type Sales, type SalesCreateInput, type SalesPage, type SalesResource, type SalesSearchQuery, type SalesUpdateInput, type Scheme, type Scope, type SearchQuery, type StoredTokens, type TenantCustomCatalog, type TenantScope, type Throttle, type ThrottleOptions, type TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type UndeclarableField, type UndeclarableReason, type UndeclarableTenantField, type UndeclaredField, type UnverifiableResource, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, type VerifyFieldsOptions, assertFieldsMatch, base64ToBytes, bytesToBase64, createMockTransport, createThrottle, defineFields, generateFieldDecls, readCustomCatalog, verifyFields };
2830
+ export { type Activity, type ActivityCreateInput, type ActivityPage, type ActivityResource, type ActivitySearchQuery, type ActivityUpdateInput, 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 Contact, type ContactCreateInput, type ContactPage, type ContactResource, type ContactSearchQuery, type ContactUpdateInput, type Contract, type ContractCreateInput, type ContractPage, type ContractResource, type ContractSearchQuery, type ContractUpdateInput, type CustomDataType, type CustomFieldResource, type CustomFor, type DeclaredCatalogs, type DefinedFields, type DepartmentRef, type ErrorCategory, type Expand, type ExpandedReadRecord, type Field, type FieldBuilder, type FieldCatalogSource, type FieldDecls, type FieldDef, type FieldPage, type FieldResource, type FieldSearchQuery, type FieldTypeMismatch, type FieldValue, type FieldVerification, type GenerateFieldDeclsOptions, type GetAccessTokenOptions, type ImageContentType, type ImageOption, type ImageReadRecord, type ImageSelectedValue, type ImageSubField, type ImageValue, type ImageWriteValue, type ItemState, type Job, type JobCreateInput, type JobPage, type JobResource, type JobSearchQuery, type JobUpdateInput, type LinkValue, type MissingField, type MockHandler, type MockReply, type MockTransportOptions, type Opportunity, type OpportunityCreateInput, type OpportunityPage, type OpportunityResource, type OpportunitySearchQuery, type OpportunityUpdateInput, type Option, type OptionResource, type OptionSearchQuery, type Order, type Partition, type PartitionId, type PartitionPage, type PartitionResource, type PartitionSearchQuery, type Phase, type PhaseAccessor, type PhaseCreateInput, type PhasePage, type PhaseResource, type PhaseSearchQuery, type PhaseUpdateInput, 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 ReadCustomCatalogOptions, type ReadFieldAlias, type Recruiter, type RecruiterCreateInput, type RecruiterPage, type RecruiterResource, type RecruiterSearchQuery, type RecruiterUpdateInput, type ReferenceMap, type ReferenceRecord, type ResourceName, type ResourcePageOf, type ResourceType, type Resume, type ResumeCreateInput, type ResumePage, type ResumeResource, type ResumeSearchQuery, type ResumeUpdateInput, type RevokeUrlOptions, type Sales, type SalesCreateInput, type SalesPage, type SalesResource, type SalesSearchQuery, type SalesUpdateInput, type Scheme, type Scope, type SearchQuery, type StoredTokens, type TenantCustomCatalog, type TenantScope, type Throttle, type ThrottleOptions, type TokenProvider, type TokenStore, type Transport, type TransportRequest, type TransportResponse, type UndeclarableField, type UndeclarableReason, type UndeclarableTenantField, type UndeclaredField, type UnverifiableResource, type User, type UserPage, type UserRef, type UserResource, type UserSearchQuery, type VerifyFieldsOptions, assertFieldsMatch, base64ToBytes, bytesToBase64, createMockTransport, createThrottle, defineFields, generateFieldDecls, rawValue, readCustomCatalog, verifyFields };
package/dist/index.js CHANGED
@@ -722,106 +722,6 @@ var isoToPortersDate = (value) => {
722
722
  return `${m[1]}/${m[2]}/${m[3]}`;
723
723
  };
724
724
 
725
- // src/xml/encode.ts
726
- var IMAGE_CONTENT_TYPES = [
727
- "image/jpeg",
728
- "image/gif",
729
- "image/png",
730
- "image/bmp"
731
- ];
732
- var escapeXml = (s) => s.replace(
733
- /[&<>]/g,
734
- (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"
735
- );
736
- var text = (v) => typeof v === "object" && !Array.isArray(v) ? JSON.stringify(v) : String(v);
737
- var scalar = (v) => escapeXml(text(v));
738
- var IMAGE_SUBFIELDS = [
739
- "FileName",
740
- "ContentType",
741
- "Content"
742
- ];
743
- var imageInner = (value) => {
744
- if (typeof value !== "object" || Array.isArray(value)) return scalar(value);
745
- const parts = value;
746
- return IMAGE_SUBFIELDS.filter((sub) => parts[sub] !== void 0).map((sub) => `<${sub}>${escapeXml(String(parts[sub]))}</${sub}>`).join("");
747
- };
748
- var invalidValue = (alias, type, value, cause) => new PortersConfigError(
749
- `${alias}: cannot write ${JSON.stringify(value)} as ${type}`,
750
- {
751
- category: "validation",
752
- hint: `${type} values are written in ISO 8601 (e.g. "2026-09-10" / "2026-09-10T12:00:00Z"); the library converts them to PORTERS' format.`,
753
- // The underlying RangeError stays on `cause` rather than in the message: the message
754
- // already names the field, the value and the type, which is what a reader needs.
755
- context: { operation: "encode" },
756
- cause
757
- }
758
- );
759
- var converted = (alias, type, value, convert) => {
760
- try {
761
- return convert();
762
- } catch (cause) {
763
- throw invalidValue(alias, type, value, cause);
764
- }
765
- };
766
- var encodeField = (type, value, alias) => {
767
- switch (type) {
768
- // Option: the selected aliases as empty child elements. Canonical input is an
769
- // array (ADR-0017, symmetric with read); a lone string is wrapped as a 1-element
770
- // selection (fail-safe).
771
- case "Option":
772
- return (Array.isArray(value) ? value : [text(value)]).map((alias2) => `<${alias2}/>`).join("");
773
- // System[DateTime] (registration/update) is Write-restricted by PORTERS; we still
774
- // serialize it identically — rejecting the write is the input type's job (SD-3).
775
- case "DateTime":
776
- case "System[DateTime]":
777
- return scalar(
778
- converted(alias, type, value, () => isoToPortersDateTime(text(value)))
779
- );
780
- // Age shares Date's wire format (`yyyy/mm/dd`): we write the birthdate.
781
- case "Date":
782
- case "Age":
783
- return scalar(
784
- converted(alias, type, value, () => isoToPortersDate(text(value)))
785
- );
786
- // Image: the three nested sub-elements (ADR-0064 論点3).
787
- case "Image":
788
- return imageInner(value);
789
- // System[Id] / Number / User & System[Reference] / Link (all ID-only) / string Data Types all
790
- // serialize as a scalar (the string types stay distinct labels per ADR-0016).
791
- // Link の Read は 3 形の union だが、Write は ID ひとつ(`<Alias>10001</Alias>`)= User と同じ。
792
- // System[Department] は静的な Write 入力から外してあるので、ここに来るのは cast 経由のみ
793
- // (System[DateTime] と同じ扱い)。到達したときは User と同じくスカラとして書き出す。
794
- case "Link":
795
- case "System[Id]":
796
- case "Number":
797
- case "User":
798
- case "System[Reference]":
799
- case "System[Department]":
800
- case "SinglelineText":
801
- case "MultilineText":
802
- case "Mail":
803
- case "Telephone":
804
- case "URL":
805
- return scalar(value);
806
- }
807
- };
808
- var encodeItem = (prefix, fields, item) => {
809
- const parts = [];
810
- for (const [alias, value] of Object.entries(item)) {
811
- if (value === null || value === void 0) continue;
812
- const type = fields.get(alias);
813
- const inner = type === void 0 || type === null ? scalar(value) : encodeField(type, value, alias);
814
- const tag2 = qualify(prefix, alias);
815
- parts.push(`<${tag2}>${inner}</${tag2}>`);
816
- }
817
- return parts.join("");
818
- };
819
- var encodeWriteItem = (prefix, fields, item) => `<Item>${encodeItem(prefix, fields, item)}</Item>`;
820
- var buildWriteXml = (config) => {
821
- const items = config.items.map((item) => encodeWriteItem(config.prefix, config.fields, item)).join("");
822
- return `<${config.resource}>${items}</${config.resource}>`;
823
- };
824
-
825
725
  // src/xml/decode.ts
826
726
  var bareTag = (key) => key.includes(".") ? key.slice(key.indexOf(".") + 1) : key;
827
727
  var pickPrefixed = (obj, prefix, key) => asString(obj[`${prefix}.${key}`]) ?? asString(obj[key]);
@@ -900,7 +800,7 @@ var mismatch = (alias, type, wants) => new PortersResourceError(
900
800
  context: { operation: "decode" }
901
801
  }
902
802
  );
903
- var converted2 = (alias, type, value, convert) => {
803
+ var converted = (alias, type, value, convert) => {
904
804
  try {
905
805
  return convert();
906
806
  } catch (cause) {
@@ -954,7 +854,7 @@ var decodeField = (type, raw, alias) => {
954
854
  // format; System[DateTime] is Write-restricted, but that is a write-time concern.
955
855
  case "DateTime":
956
856
  case "System[DateTime]":
957
- return converted2(
857
+ return converted(
958
858
  alias,
959
859
  scalarType,
960
860
  value,
@@ -964,7 +864,7 @@ var decodeField = (type, raw, alias) => {
964
864
  // and derives the age in its UI, so the faithful value is the date itself.
965
865
  case "Date":
966
866
  case "Age":
967
- return converted2(alias, scalarType, value, () => portersDateToIso(value));
867
+ return converted(alias, scalarType, value, () => portersDateToIso(value));
968
868
  }
969
869
  };
970
870
  var decodeReferenceRecord = (raw, types) => {
@@ -984,6 +884,12 @@ var decodeReferenceRecord = (raw, types) => {
984
884
  };
985
885
 
986
886
  // src/resources/read-core.ts
887
+ var rawValue = (record, alias) => {
888
+ const rec = asRecord(record);
889
+ if (rec === void 0 || !(alias in rec)) return void 0;
890
+ const value = rec[alias];
891
+ return typeof value === "string" ? value : null;
892
+ };
987
893
  var bareAlias = (key) => key.includes(".") ? key.slice(key.indexOf(".") + 1) : key;
988
894
  var USER_SUBFIELDS = ["P_Id", "P_Type", "P_Name", "P_Mail"];
989
895
  var readFieldEntry = (prefix, alias, type) => type === "User" ? `${qualify(prefix, alias)}(${USER_SUBFIELDS.map((s) => `User.${s}`).join(",")})` : qualify(prefix, alias);
@@ -1053,6 +959,106 @@ var paginateOnce = async function* (prepare) {
1053
959
  yield* paginate(prepare());
1054
960
  };
1055
961
 
962
+ // src/xml/encode.ts
963
+ var IMAGE_CONTENT_TYPES = [
964
+ "image/jpeg",
965
+ "image/gif",
966
+ "image/png",
967
+ "image/bmp"
968
+ ];
969
+ var escapeXml = (s) => s.replace(
970
+ /[&<>]/g,
971
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"
972
+ );
973
+ var text = (v) => typeof v === "object" && !Array.isArray(v) ? JSON.stringify(v) : String(v);
974
+ var scalar = (v) => escapeXml(text(v));
975
+ var IMAGE_SUBFIELDS = [
976
+ "FileName",
977
+ "ContentType",
978
+ "Content"
979
+ ];
980
+ var imageInner = (value) => {
981
+ if (typeof value !== "object" || Array.isArray(value)) return scalar(value);
982
+ const parts = value;
983
+ return IMAGE_SUBFIELDS.filter((sub) => parts[sub] !== void 0).map((sub) => `<${sub}>${escapeXml(String(parts[sub]))}</${sub}>`).join("");
984
+ };
985
+ var invalidValue = (alias, type, value, cause) => new PortersConfigError(
986
+ `${alias}: cannot write ${JSON.stringify(value)} as ${type}`,
987
+ {
988
+ category: "validation",
989
+ hint: `${type} values are written in ISO 8601 (e.g. "2026-09-10" / "2026-09-10T12:00:00Z"); the library converts them to PORTERS' format.`,
990
+ // The underlying RangeError stays on `cause` rather than in the message: the message
991
+ // already names the field, the value and the type, which is what a reader needs.
992
+ context: { operation: "encode" },
993
+ cause
994
+ }
995
+ );
996
+ var converted2 = (alias, type, value, convert) => {
997
+ try {
998
+ return convert();
999
+ } catch (cause) {
1000
+ throw invalidValue(alias, type, value, cause);
1001
+ }
1002
+ };
1003
+ var encodeField = (type, value, alias) => {
1004
+ switch (type) {
1005
+ // Option: the selected aliases as empty child elements. Canonical input is an
1006
+ // array (ADR-0017, symmetric with read); a lone string is wrapped as a 1-element
1007
+ // selection (fail-safe).
1008
+ case "Option":
1009
+ return (Array.isArray(value) ? value : [text(value)]).map((alias2) => `<${alias2}/>`).join("");
1010
+ // System[DateTime] (registration/update) is Write-restricted by PORTERS; we still
1011
+ // serialize it identically — rejecting the write is the input type's job (SD-3).
1012
+ case "DateTime":
1013
+ case "System[DateTime]":
1014
+ return scalar(
1015
+ converted2(alias, type, value, () => isoToPortersDateTime(text(value)))
1016
+ );
1017
+ // Age shares Date's wire format (`yyyy/mm/dd`): we write the birthdate.
1018
+ case "Date":
1019
+ case "Age":
1020
+ return scalar(
1021
+ converted2(alias, type, value, () => isoToPortersDate(text(value)))
1022
+ );
1023
+ // Image: the three nested sub-elements (ADR-0064 論点3).
1024
+ case "Image":
1025
+ return imageInner(value);
1026
+ // System[Id] / Number / User & System[Reference] / Link (all ID-only) / string Data Types all
1027
+ // serialize as a scalar (the string types stay distinct labels per ADR-0016).
1028
+ // Link の Read は 3 形の union だが、Write は ID ひとつ(`<Alias>10001</Alias>`)= User と同じ。
1029
+ // System[Department] は静的な Write 入力から外してあるので、ここに来るのは cast 経由のみ
1030
+ // (System[DateTime] と同じ扱い)。到達したときは User と同じくスカラとして書き出す。
1031
+ case "Link":
1032
+ case "System[Id]":
1033
+ case "Number":
1034
+ case "User":
1035
+ case "System[Reference]":
1036
+ case "System[Department]":
1037
+ case "SinglelineText":
1038
+ case "MultilineText":
1039
+ case "Mail":
1040
+ case "Telephone":
1041
+ case "URL":
1042
+ return scalar(value);
1043
+ }
1044
+ };
1045
+ var encodeItem = (prefix, fields, item) => {
1046
+ const parts = [];
1047
+ for (const [alias, value] of Object.entries(item)) {
1048
+ if (value === null || value === void 0) continue;
1049
+ const type = fields.get(alias);
1050
+ const inner = type === void 0 || type === null ? scalar(value) : encodeField(type, value, alias);
1051
+ const tag2 = qualify(prefix, alias);
1052
+ parts.push(`<${tag2}>${inner}</${tag2}>`);
1053
+ }
1054
+ return parts.join("");
1055
+ };
1056
+ var encodeWriteItem = (prefix, fields, item) => `<Item>${encodeItem(prefix, fields, item)}</Item>`;
1057
+ var buildWriteXml = (config) => {
1058
+ const items = config.items.map((item) => encodeWriteItem(config.prefix, config.fields, item)).join("");
1059
+ return `<${config.resource}>${items}</${config.resource}>`;
1060
+ };
1061
+
1056
1062
  // src/resources/query.ts
1057
1063
  var KEYWORDS_MAX_CHARS = 100;
1058
1064
  var DELETED_CONDITION_FIELDS = /* @__PURE__ */ new Set([
@@ -2698,6 +2704,7 @@ var CUSTOM_ALIAS = /^[UA]_/;
2698
2704
  var classify = (alias, fieldType) => {
2699
2705
  if (fieldType === null) {
2700
2706
  return {
2707
+ // Stryker disable next-line StringLiteral: equivalent — the caller only tests for "declarable", so any other tag routes the same way
2701
2708
  kind: "undeclarable",
2702
2709
  entry: { alias, fieldType, reason: "unknown-field-type" }
2703
2710
  };
@@ -2706,18 +2713,21 @@ var classify = (alias, fieldType) => {
2706
2713
  const dataType = dataTypeOfFieldType(fieldType);
2707
2714
  if (dataType === void 0) {
2708
2715
  return {
2716
+ // Stryker disable next-line StringLiteral: equivalent — the caller only tests for "declarable", so any other tag routes the same way
2709
2717
  kind: "undeclarable",
2710
2718
  entry: { alias, fieldType, reason: "unknown-field-type" }
2711
2719
  };
2712
2720
  }
2713
2721
  if (dataType === null) {
2714
2722
  return {
2723
+ // Stryker disable next-line StringLiteral: equivalent — the caller only tests for "declarable", so any other tag routes the same way
2715
2724
  kind: "undeclarable",
2716
2725
  entry: { alias, fieldType, label, reason: "no-data-type" }
2717
2726
  };
2718
2727
  }
2719
2728
  if (!isDeclarable(dataType)) {
2720
2729
  return {
2730
+ // Stryker disable next-line StringLiteral: equivalent — the caller only tests for "declarable", so any other tag routes the same way
2721
2731
  kind: "undeclarable",
2722
2732
  entry: { alias, fieldType, label, reason: "not-declarable" }
2723
2733
  };
@@ -2772,7 +2782,10 @@ var undeclarableNote = (entry) => {
2772
2782
  };
2773
2783
  var resourceBlock = (catalog, includeNames) => {
2774
2784
  const entries = Object.entries(catalog.fields).sort(
2775
- ([a], [b]) => a < b ? -1 : 1
2785
+ ([a], [b]) => (
2786
+ // Stryker disable next-line EqualityOperator: equivalent — object keys are unique, so the tie branch cannot occur
2787
+ a < b ? -1 : 1
2788
+ )
2776
2789
  );
2777
2790
  const declarations = entries.map(([alias, dataType]) => {
2778
2791
  const name = includeNames ? catalog.names[alias] : void 0;
@@ -2909,6 +2922,7 @@ export {
2909
2922
  createThrottle,
2910
2923
  defineFields,
2911
2924
  generateFieldDecls,
2925
+ rawValue,
2912
2926
  readCustomCatalog,
2913
2927
  verifyFields
2914
2928
  };