@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 +2 -2
- package/dist/{aggregate-DclYgG_D.d.ts → aggregate-CePTINEt.d.ts} +93 -33
- package/dist/http.d.ts +36 -13
- package/dist/http.js +15 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +249 -52
- package/dist/index.js +295 -71
- package/dist/index.js.map +1 -1
- package/dist/presentation.d.ts +54 -0
- package/dist/presentation.js +45 -0
- package/dist/presentation.js.map +1 -0
- package/dist/testing.d.ts +24 -23
- package/dist/testing.js.map +1 -1
- package/package.json +18 -11
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
|
|
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
|
|
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()`, `
|
|
67
|
-
*
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
213
|
-
*
|
|
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
|
|
291
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
490
|
-
* stamps it at the harvest boundary with the commit version
|
|
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,
|
|
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
|
|
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}
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
18
|
-
|
|
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**:
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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":";;;;
|
|
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"]}
|