@shirudo/ddd-kit 1.2.0 → 2.0.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/README.md CHANGED
@@ -17,9 +17,9 @@ Composable TypeScript toolkit for tactical Domain-Driven Design. Ships the canon
17
17
  - **Domain Events:** typed, deeply frozen, carry metadata for traceability and schema evolution.
18
18
  - **Repositories:** technology-agnostic persistence ports with an Identity-Map contract and OCC.
19
19
  - **CQRS:** zero-config in-memory `CommandBus` / `QueryBus`, plus `CommandHandler` / `QueryHandler` types for external brokers.
20
- - **Unit of Work:** opt-in `UnitOfWork` facade with tx-bound repositories, repository-side enrollment, a per-operation Identity Map, and aggregate-level dirty tracking (`changedKeys` / `hasChanges`) for partial writes honestly speaking: a transaction coordinator with registration and Identity Map; writes stay explicit by design (no auto-flush).
20
+ - **Unit of Work:** opt-in `UnitOfWork` facade with tx-bound repositories, repository-side enrollment, a per-operation Identity Map, and aggregate-level dirty tracking (`changedKeys` / `hasChanges`) for partial writes. Honestly speaking: a transaction coordinator with registration and Identity Map; writes stay explicit by design (no auto-flush).
21
21
  - **Outbox:** `withCommit` harvests pending events inside the transaction, stamps them with the aggregate's commit version, and publishes them atomically.
22
- - **Repository contract tests:** `@shirudo/ddd-kit/testing` ships the suite every adapter must pass OCC is a testable contract, not a documented pattern.
22
+ - **Repository contract tests:** `@shirudo/ddd-kit/testing` ships the suite every adapter must pass: OCC is a testable contract, not a documented pattern.
23
23
  - **Result-first boundary:** a typed error hierarchy on [`@shirudo/base-error`](https://www.npmjs.com/package/@shirudo/base-error) and `Result` from [`@shirudo/result`](https://www.npmjs.com/package/@shirudo/result); `voValidated` collects field violations and renders RFC 9457 via the opt-in `@shirudo/ddd-kit/http` entry.
24
24
 
25
25
  ## Installation
@@ -63,8 +63,10 @@ interface IdGenerator<Tag extends string> {
63
63
  * subclass: the kit can't know your invariants.
64
64
  *
65
65
  * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
66
- * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
67
- * `isRetryable`, …).
66
+ * surface (timestamps, cause chains, `toJSON()`, `isRetryable`, …). For
67
+ * client-safe / localized messages, project errors through the opt-in
68
+ * `@shirudo/base-error/presentation` subpath at the boundary; the technical
69
+ * core deliberately carries no user-facing message.
68
70
  */
69
71
  declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
70
72
  }
@@ -104,6 +106,60 @@ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
104
106
  readonly eventType: string;
105
107
  constructor(eventType: string, cause?: unknown);
106
108
  }
109
+ /**
110
+ * Thrown by `withCommit` when an event harvested from an aggregate cannot
111
+ * be safely committed: it is missing `aggregateId` / `aggregateType`
112
+ * (downstream routing would break), or it carries a pre-set
113
+ * `aggregateVersion` AHEAD of the aggregate's commit version (a leaked or
114
+ * copied fixture that would advance consumer idempotency watermarks past
115
+ * real history). Both are programming bugs in how the aggregate recorded
116
+ * the event, deterministic, and fail identically on every retry.
117
+ *
118
+ * Deliberately **not** an {@link InfrastructureError} (same reasoning as
119
+ * {@link MissingHandlerError}): the failure happens after the work
120
+ * callback completed, but it is NOT transient. A `catch (e instanceof
121
+ * InfrastructureError)` retry handler, or a retrying `TransactionScope`,
122
+ * must NOT mask it or loop on it forever; it should crash loud so the
123
+ * recordEvent / createDomainEvent misuse surfaces in development. This is
124
+ * why `withCommit` throws it directly and `UnitOfWork.run` passes it
125
+ * through unchanged instead of wrapping it in `CommitError`.
126
+ */
127
+ declare class EventHarvestError extends BaseError<"EventHarvestError"> {
128
+ /** The `type` of the offending event, for programmatic routing. */
129
+ readonly eventType?: string | undefined;
130
+ constructor(message: string,
131
+ /** The `type` of the offending event, for programmatic routing. */
132
+ eventType?: string | undefined);
133
+ }
134
+ /**
135
+ * Thrown at the end of a `UnitOfWork.run` when an aggregate that was
136
+ * loaded into the identity map during the operation carries unflushed
137
+ * `pendingEvents` but was never enrolled (no `session.enrollSaved`, and
138
+ * not deleted). The almost-certain cause is a repository `save()` that
139
+ * forgot to call `enrollSaved`, or a use case that recorded events on a
140
+ * loaded aggregate and never saved it. Without this guard those events
141
+ * would be silently dropped: never harvested into the outbox, never
142
+ * published.
143
+ *
144
+ * Deliberately **not** an `InfrastructureError` (same posture as
145
+ * {@link MissingHandlerError}): a programming bug that must crash loud,
146
+ * not be absorbed by a generic infrastructure-error handler. The throw
147
+ * happens inside the transaction, so the unit of work rolls back and
148
+ * leaves no partial state.
149
+ *
150
+ * **Scope of the guard.** A best-effort runtime safety net, not a proof.
151
+ * It only sees aggregates the identity map knows about (those loaded via
152
+ * `getById`), and detects new events by comparing the pending-event COUNT
153
+ * at load against commit, which assumes the kit's append-only event model
154
+ * (so it cannot see events that were recorded and then cleared within the
155
+ * same run). A freshly *created* aggregate that was never enrolled is
156
+ * invisible to the kit. The repository contract test suite remains the
157
+ * full mitigation. See the Unit of Work guide.
158
+ */
159
+ declare class UnenrolledChangesError extends BaseError<"UnenrolledChangesError"> {
160
+ readonly aggregateId: string;
161
+ constructor(aggregateId: string);
162
+ }
107
163
  /**
108
164
  * Thrown when an aggregate that was deleted within the current unit of
109
165
  * work is saved or re-registered again in the same operation: by
@@ -134,10 +190,16 @@ declare class AggregateDeletedError extends BaseError<"AggregateDeletedError"> {
134
190
  *
135
191
  * Not retryable: retrying won't make the row appear.
136
192
  */
193
+ interface AggregateNotFoundErrorOptions {
194
+ readonly aggregateType: string;
195
+ readonly id: string;
196
+ /** Optional lower-level error to preserve in the cause chain. */
197
+ readonly cause?: unknown;
198
+ }
137
199
  declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
138
200
  readonly aggregateType: string;
139
201
  readonly id: string;
140
- constructor(aggregateType: string, id: string, cause?: unknown);
202
+ constructor(options: AggregateNotFoundErrorOptions);
141
203
  }
142
204
  /**
143
205
  * Thrown by a repository's `save()` INSERT path when a row with the
@@ -158,10 +220,16 @@ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFo
158
220
  * idempotency-key flows load the existing aggregate and treat the
159
221
  * request as already-applied.
160
222
  */
223
+ interface DuplicateAggregateErrorOptions {
224
+ readonly aggregateType: string;
225
+ readonly aggregateId: string;
226
+ /** Optional driver-level error to preserve in the cause chain. */
227
+ readonly cause?: unknown;
228
+ }
161
229
  declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggregateError"> {
162
230
  readonly aggregateType: string;
163
231
  readonly aggregateId: string;
164
- constructor(aggregateType: string, aggregateId: string, cause?: unknown);
232
+ constructor(options: DuplicateAggregateErrorOptions);
165
233
  }
166
234
  /**
167
235
  * Thrown by `IRepository.save()` when the aggregate's expected version
@@ -172,7 +240,7 @@ declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggr
172
240
  *
173
241
  * **Retry means a FRESH unit of work** (a new `UnitOfWork.run()` /
174
242
  * `withCommit` invocation): reload, re-apply, save. Do NOT catch this
175
- * inside the same `run()` callback and continue the failed aggregate
243
+ * inside the same `run()` callback and continue: the failed aggregate
176
244
  * is already enrolled (its events would be committed for a write that
177
245
  * never happened) and the identity map still serves the same stale
178
246
  * instance to any in-place "reload".
@@ -181,18 +249,26 @@ declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggr
181
249
  * rule) detects the race. Marks itself as `retryable: true` so the
182
250
  * `isRetryable` predicate from `@shirudo/base-error` picks it up.
183
251
  */
184
- declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
252
+ interface ConcurrencyConflictErrorOptions {
185
253
  readonly aggregateType: string;
186
254
  readonly aggregateId: string;
187
255
  readonly expectedVersion: number;
188
256
  readonly actualVersion: number;
257
+ /** Optional driver-level error to preserve in the cause chain. */
258
+ readonly cause?: unknown;
259
+ }
260
+ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
189
261
  /**
190
262
  * Marks this error as retryable so `isRetryable(err)` returns
191
263
  * true. The canonical OCC pattern is to reload the aggregate, re-apply
192
264
  * the use case, and retry on this exception.
193
265
  */
194
266
  readonly retryable: true;
195
- constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
267
+ readonly aggregateType: string;
268
+ readonly aggregateId: string;
269
+ readonly expectedVersion: number;
270
+ readonly actualVersion: number;
271
+ constructor(options: ConcurrencyConflictErrorOptions);
196
272
  }
197
273
 
198
274
  /**
@@ -209,9 +285,8 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
209
285
  */
210
286
  type EventIdFactory = () => string;
211
287
  /**
212
- * Replaces the global event-id factory used by `createDomainEvent` and
213
- * `createDomainEventWithMetadata`. Call once during application bootstrap,
214
- * for example:
288
+ * Replaces the global event-id factory used by `createDomainEvent`. Call
289
+ * once during application bootstrap, for example:
215
290
  *
216
291
  * ```ts
217
292
  * import { ulid } from "ulid";
@@ -287,9 +362,8 @@ declare function resetEventIdFactory(): void;
287
362
  */
288
363
  type ClockFactory = () => Date;
289
364
  /**
290
- * Replaces the global clock factory used by `createDomainEvent` and
291
- * `createDomainEventWithMetadata`. Call once during application bootstrap
292
- * (or per-test in deterministic test suites):
365
+ * Replaces the global clock factory used by `createDomainEvent`. Call once
366
+ * during application bootstrap (or per-test in deterministic test suites):
293
367
  *
294
368
  * ```ts
295
369
  * import { setClockFactory } from "@shirudo/ddd-kit";
@@ -379,7 +453,7 @@ interface EventMetadata {
379
453
  * transport concerns the outbox needs (`aggregateId`, `aggregateType`,
380
454
  * `aggregateVersion`, `metadata`). That is the line: further transport
381
455
  * fields (partition keys, tenancy, schema URNs, …) belong in an outbox
382
- * envelope / `metadata`, not on the domain event the next first-class
456
+ * envelope / `metadata`, not on the domain event: the next first-class
383
457
  * transport field forces an `OutboxMessage` envelope port instead.
384
458
  *
385
459
  * @template T - The event type name (e.g., "OrderCreated")
@@ -422,7 +496,7 @@ interface DomainEvent<T extends string, P = void> {
422
496
  * Required for safe schema migration in event-sourced systems.
423
497
  * Use 1 for the initial schema version.
424
498
  *
425
- * **NOT the aggregate's version** that is
499
+ * **NOT the aggregate's version**: that is
426
500
  * {@link aggregateVersion}. The two are deliberately distinct
427
501
  * fields: this one says "which shape does the payload have"
428
502
  * (upcasting), the other says "which state revision of the
@@ -435,7 +509,7 @@ interface DomainEvent<T extends string, P = void> {
435
509
  * `withCommit` at the harvest boundary (all events of one aggregate
436
510
  * in one commit share it; their relative order within the commit is
437
511
  * the harvest order), or set manually via
438
- * `CreateDomainEventOptions.aggregateVersion` a pre-set value is
512
+ * `CreateDomainEventOptions.aggregateVersion`; a pre-set value is
439
513
  * never overwritten.
440
514
  *
441
515
  * Consumers use it for ordering ("apply projections up to aggregate
@@ -486,8 +560,8 @@ interface CreateDomainEventOptions {
486
560
  version?: number;
487
561
  /**
488
562
  * Pre-set the producing aggregate's version (see
489
- * `DomainEvent.aggregateVersion`). Normally left unset `withCommit`
490
- * stamps it at the harvest boundary with the commit version but
563
+ * `DomainEvent.aggregateVersion`). Normally left unset (`withCommit`
564
+ * stamps it at the harvest boundary with the commit version), but
491
565
  * useful for replay fixtures and events constructed outside an
492
566
  * aggregate. A pre-set value is never overwritten by the harvest.
493
567
  */
@@ -529,20 +603,6 @@ interface CreateDomainEventOptions {
529
603
  */
530
604
  declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
531
605
  declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
532
- /**
533
- * Creates a domain event with metadata for traceability.
534
- * Convenience function for creating events with correlation and causation IDs.
535
- *
536
- * @example
537
- * ```typescript
538
- * const event = createDomainEventWithMetadata(
539
- * "OrderCreated",
540
- * { orderId: "123" },
541
- * { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
542
- * );
543
- * ```
544
- */
545
- declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
546
606
  /**
547
607
  * Copies metadata from a source event to a new event.
548
608
  * Useful for maintaining correlation chains in event-driven architectures.
@@ -659,4 +719,4 @@ declare function sameVersion<TId extends Id<string>>(a: {
659
719
  version: Version;
660
720
  }): boolean;
661
721
 
662
- export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d, setEventIdFactory as e, type ClockFactory as f, setClockFactory as g, withClockFactory as h, resetClockFactory as i, type EventMetadata as j, type DomainEvent as k, createDomainEvent as l, createDomainEventWithMetadata as m, copyMetadata as n, mergeMetadata as o, AggregateDeletedError as p, AggregateNotFoundError as q, resetEventIdFactory as r, sameVersion as s, DuplicateAggregateError as t, ConcurrencyConflictError as u, type IdGenerator as v, withEventIdFactory as w };
722
+ export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, UnenrolledChangesError as U, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d, setEventIdFactory as e, type ClockFactory as f, setClockFactory as g, withClockFactory as h, resetClockFactory as i, type EventMetadata as j, type DomainEvent as k, createDomainEvent as l, copyMetadata as m, mergeMetadata as n, EventHarvestError as o, AggregateDeletedError as p, type AggregateNotFoundErrorOptions as q, resetEventIdFactory as r, sameVersion as s, AggregateNotFoundError as t, type DuplicateAggregateErrorOptions as u, DuplicateAggregateError as v, withEventIdFactory as w, type ConcurrencyConflictErrorOptions as x, ConcurrencyConflictError as y, type IdGenerator as z };
package/dist/http.d.ts CHANGED
@@ -1,31 +1,54 @@
1
- import { ValidationError, ProblemDetailsOptions, ProblemDetails } from '@shirudo/base-error';
1
+ import { ValidationError } from '@shirudo/base-error';
2
+ import { ProblemDetailsExtensions, ProblemDetails } from '@shirudo/base-error/problem-details';
2
3
 
3
4
  /** Extension member that carries the collected field issues. */
4
5
  type ValidationProblemMember = "errors" | "invalid-params";
5
6
  /**
6
- * Options for {@link toProblemDetails}. Mirrors base-error's
7
- * `ProblemDetailsOptions` but takes over the `extensions` member to attach the
8
- * collected field issues, and adds {@link member} to choose the wire key.
7
+ * Options for {@link toProblemDetails}: the standard RFC 9457 members the
8
+ * boundary may set, plus {@link member} to choose the wire key for the issues
9
+ * and {@link extensions} for extra public members merged alongside them.
9
10
  */
10
- interface ValidationProblemOptions extends Omit<ProblemDetailsOptions, "extensions"> {
11
+ interface ValidationProblemOptions {
12
+ /** URI reference identifying the problem type. Defaults to `"about:blank"`. */
13
+ type?: string;
14
+ /** Short, human-readable summary. Default `"Validation Failed"`. */
15
+ title?: string;
16
+ /** HTTP status code. Default `422`. */
17
+ status?: number;
18
+ /** Human-readable explanation specific to this occurrence. */
19
+ detail?: string;
20
+ /** URI reference identifying this specific occurrence. */
21
+ instance?: string;
11
22
  /**
12
23
  * Extension member that carries the field issues. Default `"errors"`
13
24
  * (`{ message, path, code?, pointer? }` entries). RFC 9457 does not
14
25
  * standardize a multi-error member; `errors` is the common convention.
15
26
  */
16
27
  member?: ValidationProblemMember;
17
- /** Extra public extension members merged alongside the issues. */
18
- extensions?: Record<string, unknown>;
28
+ /**
29
+ * Extra public extension members merged alongside the issues. JSON-safe
30
+ * by contract (RFC 9457 bodies must serialize); a trace id, for example,
31
+ * is passed here, not as a recognized top-level field.
32
+ */
33
+ extensions?: ProblemDetailsExtensions;
19
34
  }
20
35
  /**
21
36
  * Projects a base-error {@link ValidationError} to an RFC 9457 Problem Details
22
37
  * object with the collected field issues attached under an extension member.
38
+ * The return type is base-error's own
39
+ * {@link ProblemDetails} (from `@shirudo/base-error/problem-details`), so the
40
+ * RFC 9457 shape stays a single source of truth across the ecosystem.
23
41
  *
24
- * base-error is **safe by default**: `ValidationError.toProblemDetails()` does
25
- * not expose the issues on its own: they only cross to a client through the
26
- * `publicIssues()` whitelist. This helper performs that explicit projection and
42
+ * base-error is **safe by default**: the issues only cross to a client through
43
+ * the `publicIssues()` whitelist (`{ message, path, code?, pointer? }`, never
44
+ * raw validator extras). This helper performs that explicit projection and
27
45
  * applies sensible validation defaults (`422`, `"Validation Failed"`), so the
28
- * common boundary case is a one-liner instead of a footgun.
46
+ * common boundary case is a one-liner instead of a footgun. The full-fidelity
47
+ * issues remain available for observability via `error.toLogObject()`.
48
+ *
49
+ * For the general error-to-Problem-Details mapping (a public-code catalog with
50
+ * per-code `type` / `status`), use base-error's `defineProblemDetailsAdapter`
51
+ * over a `PublicErrorView`. This helper is the narrow validation shortcut.
29
52
  *
30
53
  * This is a presentation/transport concern and ships from the opt-in
31
54
  * `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.
@@ -37,10 +60,10 @@ interface ValidationProblemOptions extends Omit<ProblemDetailsOptions, "extensio
37
60
  * if (result.isErr()) {
38
61
  * return Response.json(toProblemDetails(result.error), { status: 422 });
39
62
  * }
40
- * // → { type, title: "Validation Failed", status: 422,
63
+ * // → { type: "about:blank", title: "Validation Failed", status: 422,
41
64
  * // errors: [{ message: "must be a valid email", path: ["email"], pointer: "email" }] }
42
65
  * ```
43
66
  */
44
- declare function toProblemDetails(error: ValidationError, options?: ValidationProblemOptions): ProblemDetails;
67
+ declare function toProblemDetails(error: ValidationError, options?: ValidationProblemOptions): ProblemDetails<never, ProblemDetailsExtensions>;
45
68
 
46
69
  export { type ValidationProblemMember, type ValidationProblemOptions, toProblemDetails };
package/dist/http.js CHANGED
@@ -3,13 +3,21 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/http/problem-details.ts
5
5
  function toProblemDetails(error, options = {}) {
6
- const { member = "errors", extensions, ...rest } = options;
7
- return error.toProblemDetails({
8
- title: "Validation Failed",
9
- status: 422,
10
- ...rest,
11
- extensions: { ...extensions, [member]: error.publicIssues() }
12
- });
6
+ const {
7
+ member = "errors",
8
+ extensions,
9
+ type = "about:blank",
10
+ title = "Validation Failed",
11
+ status = 422,
12
+ detail,
13
+ instance
14
+ } = options;
15
+ const problem = { type, title, status };
16
+ if (detail !== void 0) problem.detail = detail;
17
+ if (instance !== void 0) problem.instance = instance;
18
+ if (extensions) Object.assign(problem, extensions);
19
+ problem[member] = error.publicIssues();
20
+ return problem;
13
21
  }
14
22
  __name(toProblemDetails, "toProblemDetails");
15
23
 
package/dist/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/http/problem-details.ts"],"names":[],"mappings":";;;;AAkDO,SAAS,gBAAA,CACf,KAAA,EACA,OAAA,GAAoC,EAAC,EACpB;AACjB,EAAA,MAAM,EAAE,MAAA,GAAS,QAAA,EAAU,UAAA,EAAY,GAAG,MAAK,GAAI,OAAA;AACnD,EAAA,OAAO,MAAM,gBAAA,CAAiB;AAAA,IAC7B,KAAA,EAAO,mBAAA;AAAA,IACP,MAAA,EAAQ,GAAA;AAAA,IACR,GAAG,IAAA;AAAA,IACH,UAAA,EAAY,EAAE,GAAG,UAAA,EAAY,CAAC,MAAM,GAAG,KAAA,CAAM,YAAA,EAAa;AAAE,GAC5D,CAAA;AACF;AAXgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA","file":"http.js","sourcesContent":["import type {\n\tProblemDetails,\n\tProblemDetailsOptions,\n\tValidationError,\n} from \"@shirudo/base-error\";\n\n/** Extension member that carries the collected field issues. */\nexport type ValidationProblemMember = \"errors\" | \"invalid-params\";\n\n/**\n * Options for {@link toProblemDetails}. Mirrors base-error's\n * `ProblemDetailsOptions` but takes over the `extensions` member to attach the\n * collected field issues, and adds {@link member} to choose the wire key.\n */\nexport interface ValidationProblemOptions\n\textends Omit<ProblemDetailsOptions, \"extensions\"> {\n\t/**\n\t * Extension member that carries the field issues. Default `\"errors\"`\n\t * (`{ message, path, code?, pointer? }` entries). RFC 9457 does not\n\t * standardize a multi-error member; `errors` is the common convention.\n\t */\n\tmember?: ValidationProblemMember;\n\t/** Extra public extension members merged alongside the issues. */\n\textensions?: Record<string, unknown>;\n}\n\n/**\n * Projects a base-error {@link ValidationError} to an RFC 9457 Problem Details\n * object with the collected field issues attached under an extension member.\n *\n * base-error is **safe by default**: `ValidationError.toProblemDetails()` does\n * not expose the issues on its own: they only cross to a client through the\n * `publicIssues()` whitelist. This helper performs that explicit projection and\n * applies sensible validation defaults (`422`, `\"Validation Failed\"`), so the\n * common boundary case is a one-liner instead of a footgun.\n *\n * This is a presentation/transport concern and ships from the opt-in\n * `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.\n *\n * @example\n * ```ts\n * import { toProblemDetails } from \"@shirudo/ddd-kit/http\";\n *\n * if (result.isErr()) {\n * return Response.json(toProblemDetails(result.error), { status: 422 });\n * }\n * // → { type, title: \"Validation Failed\", status: 422,\n * // errors: [{ message: \"must be a valid email\", path: [\"email\"], pointer: \"email\" }] }\n * ```\n */\nexport function toProblemDetails(\n\terror: ValidationError,\n\toptions: ValidationProblemOptions = {},\n): ProblemDetails {\n\tconst { member = \"errors\", extensions, ...rest } = options;\n\treturn error.toProblemDetails({\n\t\ttitle: \"Validation Failed\",\n\t\tstatus: 422,\n\t\t...rest,\n\t\textensions: { ...extensions, [member]: error.publicIssues() },\n\t});\n}\n"]}
1
+ {"version":3,"sources":["../src/http/problem-details.ts"],"names":[],"mappings":";;;;AAuEO,SAAS,gBAAA,CACf,KAAA,EACA,OAAA,GAAoC,EAAC,EACa;AAClD,EAAA,MAAM;AAAA,IACL,MAAA,GAAS,QAAA;AAAA,IACT,UAAA;AAAA,IACA,IAAA,GAAO,aAAA;AAAA,IACP,KAAA,GAAQ,mBAAA;AAAA,IACR,MAAA,GAAS,GAAA;AAAA,IACT,MAAA;AAAA,IACA;AAAA,GACD,GAAI,OAAA;AAEJ,EAAA,MAAM,OAAA,GAAmC,EAAE,IAAA,EAAM,KAAA,EAAO,MAAA,EAAO;AAC/D,EAAA,IAAI,MAAA,KAAW,MAAA,EAAW,OAAA,CAAQ,MAAA,GAAS,MAAA;AAC3C,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAA,CAAQ,QAAA,GAAW,QAAA;AAC/C,EAAA,IAAI,UAAA,EAAY,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,UAAU,CAAA;AAIjD,EAAA,OAAA,CAAQ,MAAM,CAAA,GAAI,KAAA,CAAM,YAAA,EAAa;AAErC,EAAA,OAAO,OAAA;AACR;AAxBgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA","file":"http.js","sourcesContent":["import type { ValidationError } from \"@shirudo/base-error\";\nimport type {\n\tProblemDetails,\n\tProblemDetailsExtensions,\n} from \"@shirudo/base-error/problem-details\";\n\n/** Extension member that carries the collected field issues. */\nexport type ValidationProblemMember = \"errors\" | \"invalid-params\";\n\n/**\n * Options for {@link toProblemDetails}: the standard RFC 9457 members the\n * boundary may set, plus {@link member} to choose the wire key for the issues\n * and {@link extensions} for extra public members merged alongside them.\n */\nexport interface ValidationProblemOptions {\n\t/** URI reference identifying the problem type. Defaults to `\"about:blank\"`. */\n\ttype?: string;\n\t/** Short, human-readable summary. Default `\"Validation Failed\"`. */\n\ttitle?: string;\n\t/** HTTP status code. Default `422`. */\n\tstatus?: number;\n\t/** Human-readable explanation specific to this occurrence. */\n\tdetail?: string;\n\t/** URI reference identifying this specific occurrence. */\n\tinstance?: string;\n\t/**\n\t * Extension member that carries the field issues. Default `\"errors\"`\n\t * (`{ message, path, code?, pointer? }` entries). RFC 9457 does not\n\t * standardize a multi-error member; `errors` is the common convention.\n\t */\n\tmember?: ValidationProblemMember;\n\t/**\n\t * Extra public extension members merged alongside the issues. JSON-safe\n\t * by contract (RFC 9457 bodies must serialize); a trace id, for example,\n\t * is passed here, not as a recognized top-level field.\n\t */\n\textensions?: ProblemDetailsExtensions;\n}\n\n/**\n * Projects a base-error {@link ValidationError} to an RFC 9457 Problem Details\n * object with the collected field issues attached under an extension member.\n * The return type is base-error's own\n * {@link ProblemDetails} (from `@shirudo/base-error/problem-details`), so the\n * RFC 9457 shape stays a single source of truth across the ecosystem.\n *\n * base-error is **safe by default**: the issues only cross to a client through\n * the `publicIssues()` whitelist (`{ message, path, code?, pointer? }`, never\n * raw validator extras). This helper performs that explicit projection and\n * applies sensible validation defaults (`422`, `\"Validation Failed\"`), so the\n * common boundary case is a one-liner instead of a footgun. The full-fidelity\n * issues remain available for observability via `error.toLogObject()`.\n *\n * For the general error-to-Problem-Details mapping (a public-code catalog with\n * per-code `type` / `status`), use base-error's `defineProblemDetailsAdapter`\n * over a `PublicErrorView`. This helper is the narrow validation shortcut.\n *\n * This is a presentation/transport concern and ships from the opt-in\n * `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.\n *\n * @example\n * ```ts\n * import { toProblemDetails } from \"@shirudo/ddd-kit/http\";\n *\n * if (result.isErr()) {\n * return Response.json(toProblemDetails(result.error), { status: 422 });\n * }\n * // → { type: \"about:blank\", title: \"Validation Failed\", status: 422,\n * // errors: [{ message: \"must be a valid email\", path: [\"email\"], pointer: \"email\" }] }\n * ```\n */\nexport function toProblemDetails(\n\terror: ValidationError,\n\toptions: ValidationProblemOptions = {},\n): ProblemDetails<never, ProblemDetailsExtensions> {\n\tconst {\n\t\tmember = \"errors\",\n\t\textensions,\n\t\ttype = \"about:blank\",\n\t\ttitle = \"Validation Failed\",\n\t\tstatus = 422,\n\t\tdetail,\n\t\tinstance,\n\t} = options;\n\n\tconst problem: Record<string, unknown> = { type, title, status };\n\tif (detail !== undefined) problem.detail = detail;\n\tif (instance !== undefined) problem.instance = instance;\n\tif (extensions) Object.assign(problem, extensions);\n\t// `PublicIssue.path` is `ReadonlyArray<PropertyKey>`, so the issue array is\n\t// not statically a `ProblemDetailsJsonValue`; the wire form only ever\n\t// carries string/number path segments, so this is JSON-safe in practice.\n\tproblem[member] = error.publicIssues();\n\n\treturn problem as ProblemDetails<never, ProblemDetailsExtensions>;\n}\n"]}