@nwire/handler 0.8.0 → 0.9.1

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
@@ -1,88 +1,203 @@
1
1
  # @nwire/handler
2
2
 
3
- > Operation primitive — one shape for action / query / resolver, usable from any transport.
4
-
5
- `defineHandler(name, config)` describes a single operation: input
6
- schema, response shape, the function. `execute(handler, input, ctx?)`
7
- runs one. Every Nwire transport (`@nwire/http`, `@nwire/queue`,
8
- `@nwire/mcp`) speaks this exact shape; `@nwire/forge`'s `defineAction`
9
- and `defineQuery` are flavored sugar on top.
3
+ The operation primitive — one typed, callable, hookable value usable from every transport (HTTP, queue, MCP, CLI, direct test).
10
4
 
11
5
  ```bash
12
- pnpm add @nwire/handler zod
6
+ pnpm add @nwire/handler
7
+ ```
8
+
9
+ ## Why
10
+
11
+ Every backend ends up with the same shape: parse input, run a function with some deps, return a typed response or throw a typed error. Different teams wrap it in different abstractions (controllers, route handlers, command handlers, tRPC procedures, NestJS providers). They're all the same thing.
12
+
13
+ `@nwire/handler` is that thing, distilled: a callable, type-generic value. It's a `Hook` from `@nwire/hooks` underneath, so it gets `.use()` middleware, `.on()` listeners, `.tap()` observation, `signal` cancellation, and replay — for free.
14
+
15
+ Three rules:
16
+
17
+ 1. **Ctx is generic.** Whatever you pass to `.run(ctx)` is what the handler body destructures, fully typed.
18
+ 2. **App pins ctx once.** `defineHandlerWith<AppExtras>()` returns a factory you import everywhere; handlers never annotate ctx.
19
+ 3. **No global pollution.** Plugin contributions are imported types, intersected at the app boundary. Nothing is `declare module`-ed.
20
+
21
+ ## Surface
22
+
23
+ ```ts
24
+ // The primitive
25
+ function defineHandler<TInputSchema, TOutput, TExtras extends object = object>(
26
+ name: string,
27
+ config: HandlerConfig<TInputSchema, TOutput, TExtras>,
28
+ ): HandlerDefinition<TInputSchema, TOutput, TExtras>;
29
+
30
+ // The app-boundary factory — pins TExtras once
31
+ function defineHandlerWith<TExtras extends object>(): typeof defineHandler bound to TExtras;
32
+
33
+ // Type alias for declaring a handler's ctx shape inline
34
+ type Ctx<TInput, TExtras extends object = object> =
35
+ { input: TInput; signal: AbortSignal; set: … } & TExtras;
36
+
37
+ // Resources, errors, response factories
38
+ function defineResource(name, { schema, public, examples });
39
+ function defineError({ code, status, summary }); // callable: throw NotFound({ resourceId });
40
+ ok / created / accepted / noContent / notModified / gone
41
+
42
+ // Built-in errors
43
+ Unauthorized / Forbidden / NotFound / Conflict / Gone / BadRequest
44
+
45
+ // Type helpers
46
+ HandlerInput<H> / HandlerOutput<H> / HandlerExtras<H>
13
47
  ```
14
48
 
15
- ## Quick example
49
+ ## Consumer example
50
+
51
+ `app/define.ts` — one file, pinned once:
52
+
53
+ ```ts
54
+ import { defineHandlerWith, type Ctx } from "@nwire/handler";
55
+ import type { AuthCradle } from "@nwire/auth";
56
+ import type { DbCradle } from "@nwire/data-drizzle";
57
+
58
+ // Plugins export type fragments; app composes its cradle:
59
+ export type AppCradle = AuthCradle &
60
+ DbCradle & {
61
+ config: AppConfig;
62
+ logger: Logger;
63
+ };
64
+
65
+ // Extras the wires put onto ctx at request time:
66
+ type AppExtras = {
67
+ logger: Logger;
68
+ pg: PgClient;
69
+ user: { id: string; role: string; balance: number };
70
+ };
71
+
72
+ export const defineAction = defineHandlerWith<AppExtras>();
73
+ export type AppCtx<I = unknown> = Ctx<I, AppExtras>;
74
+ ```
75
+
76
+ `app/orders/buy-wash.action.ts` — a complete handler:
16
77
 
17
78
  ```ts
18
79
  import { z } from "zod";
19
- import {
20
- defineHandler,
21
- defineMiddleware,
22
- defineHook,
23
- pipe,
24
- execute,
25
- NotFound,
26
- ok,
27
- } from "@nwire/handler";
28
-
29
- const authenticate = defineMiddleware("authenticate", async (ctx, next) => {
30
- ctx.user = await verifyToken(ctx.header("authorization"));
80
+ import { defineResource, defineError, NotFound } from "@nwire/handler";
81
+ import { defineAction } from "@/define";
82
+
83
+ const Wash = defineResource("Wash", {
84
+ schema: z.object({ id: z.string(), name: z.string(), price: z.number() }),
85
+ public: ["id", "name", "price"],
86
+ });
87
+
88
+ const InsufficientFunds = defineError({
89
+ code: "INSUFFICIENT_FUNDS",
90
+ status: 402,
91
+ summary: "balance too low for this wash",
92
+ });
93
+
94
+ export const BuyWash = defineAction("buyWash", {
95
+ input: z.object({ washId: z.string() }),
96
+ returns: Wash,
97
+ errors: [NotFound, InsufficientFunds],
98
+ handler: async ({ input, logger, pg, user, signal }) => {
99
+ const row = await pg.query("SELECT * FROM washes WHERE id=$1", [input.washId], { signal });
100
+ if (!row) throw NotFound({ resourceId: input.washId });
101
+ if (user.balance < row.price) throw InsufficientFunds({ have: user.balance, need: row.price });
102
+ logger.log(`${user.id} bought ${row.id}`);
103
+ return row;
104
+ },
105
+ });
106
+
107
+ // Per-handler middleware via the hook substrate
108
+ BuyWash.use(async (ctx, next) => {
109
+ const t = performance.now();
31
110
  await next();
111
+ ctx.logger.log(`buyWash ${(performance.now() - t).toFixed(1)}ms`);
32
112
  });
113
+ ```
33
114
 
34
- const auditLog = defineHook("after", "audit", async (ctx, result) => {
35
- await audit.write({ user: ctx.user.id, result });
115
+ `wires/api.wire.ts` boot, container, request composition:
116
+
117
+ ```ts
118
+ import { createContainer } from "@nwire/container";
119
+ import type { AppCradle } from "@/define";
120
+ import { BuyWash } from "@/orders/buy-wash.action";
121
+
122
+ const root = createContainer<AppCradle>();
123
+ root.register("config", { port: 3000 });
124
+ root.register("logger", () => ({ log: (s) => console.log(s) }));
125
+ root.register("db.pg", () => new PgClient(root.cradle.config.port));
126
+
127
+ const server = createServer(async (req, res) => {
128
+ const user = await authenticate(req);
129
+
130
+ const result = await BuyWash(JSON.parse(await readBody(req))).run({
131
+ ctx: {
132
+ logger: root.cradle.logger,
133
+ pg: root.cradle["db.pg"],
134
+ user,
135
+ },
136
+ signal: req.signal,
137
+ });
138
+ res.writeHead(201).end(JSON.stringify(result));
36
139
  });
140
+ ```
37
141
 
38
- const userPipeline = pipe("user-pipeline", authenticate, auditLog);
142
+ `app/orders/buy-wash.test.ts` unit test, no container:
39
143
 
40
- const getUser = defineHandler("getUser", {
41
- input: z.object({ id: z.string() }),
42
- middleware: [userPipeline],
43
- handler: async ({ input, resolve }) => {
44
- const repo = resolve<UserRepo>("UserRepo");
45
- const user = await repo.findById(input.id);
46
- if (!user) throw NotFound("user", input.id);
47
- return ok(user);
48
- },
144
+ ```ts
145
+ import { expect, test } from "vitest";
146
+ import { BuyWash, InsufficientFunds } from "./buy-wash.action";
147
+
148
+ test("rejects when balance < price", async () => {
149
+ await expect(
150
+ BuyWash({ washId: "w1" }).run({
151
+ ctx: {
152
+ logger: { log: () => {} },
153
+ pg: { query: async () => ({ id: "w1", name: "Quick", price: 100 }) },
154
+ user: { id: "u-1", role: "user", balance: 5 },
155
+ },
156
+ }),
157
+ ).rejects.toMatchObject({
158
+ code: "INSUFFICIENT_FUNDS",
159
+ status: 402,
160
+ context: { have: 5, need: 100 },
161
+ });
49
162
  });
163
+ ```
164
+
165
+ ## The four contracts in one model
166
+
167
+ | Concern | How |
168
+ | ---------------- | ------------------------------------------------------------------------------------------- |
169
+ | Input validation | `input: zodSchema` — parsed before the handler body runs |
170
+ | Output shape | `returns: Resource \| ResponseSpec[]` — narrows the handler's return type, drives OpenAPI |
171
+ | Throws | `errors: [NotFound, …]` — typed throwables, drive 4xx OpenAPI bodies |
172
+ | Cancellation | `signal: AbortSignal` on ctx — forward to fetch/pg/etc., bail via `signal.throwIfAborted()` |
173
+
174
+ ## Cancellation in practice
50
175
 
51
- const res = await execute(getUser, { id: "u_1" }, { resolve });
176
+ The signal flows through nested `.run()` calls automatically:
177
+
178
+ ```ts
179
+ const ParentOp = defineAction("parent", {
180
+ handler: async ({ signal }) => ChildOp().run({ signal }),
181
+ });
182
+ // caller-supplied signal → ParentOp.run({ signal }) → ChildOp.run({ signal })
183
+ // abort the caller's controller → both bail
52
184
  ```
53
185
 
54
- ## Surface
186
+ When no signal is supplied, ctx gets a non-aborting placeholder — code can always pass `ctx.signal` through to `fetch`/`pg`/`redis` without null checks.
187
+
188
+ ## Errors
189
+
190
+ ```ts
191
+ throw NotFound; // bare value (it IS an Error)
192
+ throw NotFound({ resourceId: input.id }); // callable, contextualised clone
193
+ ```
194
+
195
+ Caught at the transport layer — REST → `status` + `{ code, message, context }`, GraphQL → `extensions.code`, CLI → exit status + stderr.
196
+
197
+ ## Built on @nwire/hooks
198
+
199
+ Every handler is a `Hook<HandlerRunCtx>` underneath. `.use()`, `.on()`, `.tap()`, `.runDetailed()` all work. The user's handler function is the innermost chain step, registered at construction with `Number.MIN_SAFE_INTEGER` priority so every `.use()` you add wraps around it. Telemetry, replay, observation come along for free.
200
+
201
+ ## Scope
55
202
 
56
- | Export | Role |
57
- | ----------------------------------- | --------------------------------------------------------------- |
58
- | `defineHandler(name, config)` | The operation primitive: `{ input, handler, returns?, errors?, summary?, policy?, middleware? }`. |
59
- | `execute(handler, input, ctx?)` | Run one handler; returns a response envelope. |
60
- | `defineMiddleware(name?, fn)` | Chain step `(ctx, next)`. Captures `$source`. Reusable. |
61
- | `defineHook("before" \| "after", name?, fn)` | Before-or-after-only step (cleaner than mw that forgets `next()`). |
62
- | `pipe(name?, ...steps)` | Compose middleware + hooks into a reusable pipe value. Attaches a per-pipe `$hook` (`@nwire/hooks`) so transports adopt it for telemetry. |
63
- | `unwindPipe(steps)` | Walk a pipe into `{ middlewares, beforeHooks, afterHooks }` — transports call this when mounting. |
64
- | `defineResource(name, opts)` | Public response shape (field allowlist, OpenAPI schema). |
65
- | `defineError(meta)` | Typed throwable with status code. |
66
- | Response factories | `ok` / `created` / `accepted` / `noContent` / `notModified` / `gone`. |
67
- | Framework errors | `Unauthorized` / `Forbidden` / `NotFound` / `Conflict` / `Gone` / `BadRequest`. |
68
- | Type guards | `isHandlerDefinition` / `isMiddleware` / `isHook` / `isPipe` / `isResourceDefinition` / `isNwireError` / `isResponseSpec` / `isResponseInstance`. |
69
-
70
- ### `$source` + `$hook`
71
-
72
- - Every `defineMiddleware` / `pipe` captures its call site as `$source`
73
- via `@nwire/messages.captureSourceLocation`, so Studio can render
74
- click-to-open chips.
75
- - Every `pipe(...)` creates a per-pipe `Hook<ResolverCtx>` (one
76
- `.use()` per middleware step). Runtimes adopt it via
77
- `runtime.adoptHook($hook)` to route taps into telemetry; consumers
78
- that don't know about `$hook` ignore the field.
79
-
80
- ## Related
81
-
82
- - `@nwire/forge` — re-exports this surface and layers `defineAction` (`defineHandler` + `emits`) + `defineQuery` on top.
83
- - `@nwire/hooks` — backs `pipe()`'s per-pipe `$hook` and the resolver-level dispatch chain.
84
- - `@nwire/http` — mounts handlers by `unwindPipe`-ing their middleware.
85
-
86
- ## Status
87
-
88
- v0.x — handler shape, middleware/hook/pipe primitives, response factories, and core errors are locked. Per-pipe `$hook` adoption is the canonical observation seam.
203
+ Standalone no DI, no events, no logger, no transport opinions. App composes ctx by value; transports compose ctx at boot. Forge / HTTP / queue / MCP wires layer on top.
@@ -7,14 +7,20 @@
7
7
  * summary: "Station already has an unfinished wash",
8
8
  * });
9
9
  *
10
- * // In a resolver handler:
11
- * if (active) throw WashInProgress;
10
+ * // Throw it with context — callable form, symmetric with defineEvent/defineAction:
11
+ * throw WashInProgress({ stationId: input.id });
12
12
  *
13
- * Each call site reuses the SAME instance (immutable). Transports detect
14
- * `instanceof NwireError`, read `.code` + `.status` + `.summary`, render
15
- * the right shape per medium:
16
- * - REST → JSON body `{ code, message }` with the declared status
17
- * - GraphQL → error with `extensions: { code }`
13
+ * // Throw it without context — works because the definition IS an Error:
14
+ * throw WashInProgress;
15
+ *
16
+ * Each `defineError(...)` result is BOTH:
17
+ * - an `Error` instance you can `throw` directly (zero context),
18
+ * - a callable factory `Boom(context)` that returns a contextualised clone.
19
+ *
20
+ * Transports detect `instanceof NwireError`, read `.code` + `.status` + `.summary`
21
+ * + `.context`, render the right shape per medium:
22
+ * - REST → JSON body `{ code, message, context }` with the declared status
23
+ * - GraphQL → error with `extensions: { code, context }`
18
24
  * - CLI → stderr message + exit status
19
25
  */
20
26
  export interface ErrorMeta {
@@ -24,36 +30,36 @@ export interface ErrorMeta {
24
30
  readonly description?: string;
25
31
  readonly tags?: readonly string[];
26
32
  }
27
- export interface ErrorDefinition extends ErrorMeta {
28
- readonly $kind: "error";
29
- }
30
33
  /**
31
34
  * Concrete Error subclass — thrown by domain code, caught by transport.
32
- * The `defineError(...)` result is an instance of this class.
35
+ * The `defineError(...)` result is an instance of this class, AND callable
36
+ * so `Boom(context)` returns a contextualised clone.
33
37
  */
34
- export declare class NwireError extends Error implements ErrorDefinition {
38
+ export declare class NwireError extends Error implements ErrorMeta {
35
39
  readonly $kind: "error";
36
40
  readonly code: string;
37
41
  readonly status: number;
38
42
  readonly summary: string;
39
43
  readonly description?: string;
40
44
  readonly tags?: readonly string[];
41
- constructor(meta: ErrorMeta);
42
- /**
43
- * Build a fresh error instance with the same metadata but additional
44
- * runtime context — e.g., the resource id that triggered it.
45
- */
46
- with(context: Record<string, unknown>): NwireError & {
47
- readonly context: Record<string, unknown>;
48
- };
45
+ readonly context?: Readonly<Record<string, unknown>>;
46
+ constructor(meta: ErrorMeta, context?: Readonly<Record<string, unknown>>);
47
+ }
48
+ /**
49
+ * The value returned by `defineError(meta)` — callable to build a contextual
50
+ * clone, and itself an Error instance you can throw directly.
51
+ */
52
+ export interface ErrorDefinition extends NwireError {
53
+ /** Build a contextualised clone — `throw NotFound({ resourceId: id })`. */
54
+ (context: Readonly<Record<string, unknown>>): NwireError;
49
55
  }
50
- export declare function defineError(meta: ErrorMeta): NwireError;
56
+ export declare function defineError(meta: ErrorMeta): ErrorDefinition;
51
57
  /** Type narrow. */
52
58
  export declare function isNwireError(x: unknown): x is NwireError;
53
- export declare const Unauthorized: NwireError;
54
- export declare const Forbidden: NwireError;
55
- export declare const NotFound: NwireError;
56
- export declare const Gone: NwireError;
57
- export declare const BadRequest: NwireError;
58
- export declare const Conflict: NwireError;
59
+ export declare const Unauthorized: ErrorDefinition;
60
+ export declare const Forbidden: ErrorDefinition;
61
+ export declare const NotFound: ErrorDefinition;
62
+ export declare const Gone: ErrorDefinition;
63
+ export declare const BadRequest: ErrorDefinition;
64
+ export declare const Conflict: ErrorDefinition;
59
65
  //# sourceMappingURL=define-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-error.d.ts","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CAKzB;AAED;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAM,YAAW,eAAe;IAC9D,QAAQ,CAAC,KAAK,EAAG,OAAO,CAAU;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;gBAEtB,IAAI,EAAE,SAAS;IAU3B;;;OAGG;IACH,IAAI,CACF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,UAAU,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;CAK9D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAEvD;AAED,mBAAmB;AACnB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAExD;AAGD,eAAO,MAAM,YAAY,YAIvB,CAAC;AAEH,eAAO,MAAM,SAAS,YAIpB,CAAC;AAEH,eAAO,MAAM,QAAQ,YAInB,CAAC;AAEH,eAAO,MAAM,IAAI,YAIf,CAAC;AAEH,eAAO,MAAM,UAAU,YAIrB,CAAC;AAEH,eAAO,MAAM,QAAQ,YAInB,CAAC"}
1
+ {"version":3,"file":"define-error.d.ts","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAM,YAAW,SAAS;IACxD,QAAQ,CAAC,KAAK,EAAG,OAAO,CAAU;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;gBAEzC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAUzE;AAED;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,2EAA2E;IAC3E,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC;CAC1D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAW5D;AAED,mBAAmB;AACnB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAExD;AAGD,eAAO,MAAM,YAAY,iBAIvB,CAAC;AAEH,eAAO,MAAM,SAAS,iBAIpB,CAAC;AAEH,eAAO,MAAM,QAAQ,iBAInB,CAAC;AAEH,eAAO,MAAM,IAAI,iBAIf,CAAC;AAEH,eAAO,MAAM,UAAU,iBAIrB,CAAC;AAEH,eAAO,MAAM,QAAQ,iBAInB,CAAC"}
@@ -7,19 +7,26 @@
7
7
  * summary: "Station already has an unfinished wash",
8
8
  * });
9
9
  *
10
- * // In a resolver handler:
11
- * if (active) throw WashInProgress;
10
+ * // Throw it with context — callable form, symmetric with defineEvent/defineAction:
11
+ * throw WashInProgress({ stationId: input.id });
12
12
  *
13
- * Each call site reuses the SAME instance (immutable). Transports detect
14
- * `instanceof NwireError`, read `.code` + `.status` + `.summary`, render
15
- * the right shape per medium:
16
- * - REST → JSON body `{ code, message }` with the declared status
17
- * - GraphQL → error with `extensions: { code }`
13
+ * // Throw it without context — works because the definition IS an Error:
14
+ * throw WashInProgress;
15
+ *
16
+ * Each `defineError(...)` result is BOTH:
17
+ * - an `Error` instance you can `throw` directly (zero context),
18
+ * - a callable factory `Boom(context)` that returns a contextualised clone.
19
+ *
20
+ * Transports detect `instanceof NwireError`, read `.code` + `.status` + `.summary`
21
+ * + `.context`, render the right shape per medium:
22
+ * - REST → JSON body `{ code, message, context }` with the declared status
23
+ * - GraphQL → error with `extensions: { code, context }`
18
24
  * - CLI → stderr message + exit status
19
25
  */
20
26
  /**
21
27
  * Concrete Error subclass — thrown by domain code, caught by transport.
22
- * The `defineError(...)` result is an instance of this class.
28
+ * The `defineError(...)` result is an instance of this class, AND callable
29
+ * so `Boom(context)` returns a contextualised clone.
23
30
  */
24
31
  export class NwireError extends Error {
25
32
  $kind = "error";
@@ -28,7 +35,8 @@ export class NwireError extends Error {
28
35
  summary;
29
36
  description;
30
37
  tags;
31
- constructor(meta) {
38
+ context;
39
+ constructor(meta, context) {
32
40
  super(meta.summary);
33
41
  this.name = meta.code;
34
42
  this.code = meta.code;
@@ -36,19 +44,19 @@ export class NwireError extends Error {
36
44
  this.summary = meta.summary;
37
45
  this.description = meta.description;
38
46
  this.tags = meta.tags;
39
- }
40
- /**
41
- * Build a fresh error instance with the same metadata but additional
42
- * runtime context — e.g., the resource id that triggered it.
43
- */
44
- with(context) {
45
- const next = new NwireError(this);
46
- Object.assign(next, { context });
47
- return next;
47
+ if (context !== undefined)
48
+ this.context = context;
48
49
  }
49
50
  }
50
51
  export function defineError(meta) {
51
- return new NwireError(meta);
52
+ // Build the canonical (no-context) instance + make it callable.
53
+ const base = new NwireError(meta);
54
+ const factory = ((context) => new NwireError(meta, context));
55
+ // Copy every own property + the prototype-resident fields onto the factory
56
+ // so `factory instanceof Error`, `factory.code`, `factory.message`, etc.
57
+ // all behave like the underlying NwireError instance.
58
+ Object.setPrototypeOf(factory, base);
59
+ return factory;
52
60
  }
53
61
  /** Type narrow. */
54
62
  export function isNwireError(x) {
@@ -1 +1 @@
1
- {"version":3,"file":"define-error.js","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAkBH;;;GAGG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,KAAK,GAAG,OAAgB,CAAC;IACzB,IAAI,CAAS;IACb,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,WAAW,CAAU;IACrB,IAAI,CAAqB;IAElC,YAAY,IAAe;QACzB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED;;;OAGG;IACH,IAAI,CACF,OAAgC;QAEhC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACjC,OAAO,IAAkE,CAAC;IAC5E,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,YAAY,CAAC,CAAU;IACrC,OAAO,CAAC,YAAY,UAAU,CAAC;AACjC,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;IACtC,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,0BAA0B;CACpC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;IACnC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,kDAAkD;CAC5D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,+BAA+B;CACzC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,WAAW,CAAC;IAC9B,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,iDAAiD;CAC3D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;IACpC,IAAI,EAAE,aAAa;IACnB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,8EAA8E;CACxF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,2DAA2D;CACrE,CAAC,CAAC"}
1
+ {"version":3,"file":"define-error.js","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAUH;;;;GAIG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,KAAK,GAAG,OAAgB,CAAC;IACzB,IAAI,CAAS;IACb,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,WAAW,CAAU;IACrB,IAAI,CAAqB;IACzB,OAAO,CAAqC;IAErD,YAAY,IAAe,EAAE,OAA2C;QACtE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACpD,CAAC;CACF;AAWD,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,gEAAgE;IAChE,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,OAAO,GAAoB,CAAC,CAAC,OAA0C,EAAE,EAAE,CAC/E,IAAI,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAA+B,CAAC;IAE/D,2EAA2E;IAC3E,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,YAAY,CAAC,CAAU;IACrC,OAAO,CAAC,YAAY,UAAU,CAAC;AACjC,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;IACtC,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,0BAA0B;CACpC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;IACnC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,kDAAkD;CAC5D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,+BAA+B;CACzC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,WAAW,CAAC;IAC9B,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,iDAAiD;CAC3D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;IACpC,IAAI,EAAE,aAAa;IACnB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,8EAA8E;CACxF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,2DAA2D;CACrE,CAAC,CAAC"}