@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 +183 -68
- package/dist/define-error.d.ts +33 -27
- package/dist/define-error.d.ts.map +1 -1
- package/dist/define-error.js +27 -19
- package/dist/define-error.js.map +1 -1
- package/dist/define-handler.d.ts +146 -107
- package/dist/define-handler.d.ts.map +1 -1
- package/dist/define-handler.js +139 -83
- package/dist/define-handler.js.map +1 -1
- package/dist/define-middleware.js +4 -3
- package/dist/define-middleware.js.map +1 -1
- package/dist/handler-index.d.ts +17 -22
- package/dist/handler-index.d.ts.map +1 -1
- package/dist/handler-index.js +16 -21
- package/dist/handler-index.js.map +1 -1
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -1,88 +1,203 @@
|
|
|
1
1
|
# @nwire/handler
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
142
|
+
`app/orders/buy-wash.test.ts` — unit test, no container:
|
|
39
143
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/define-error.d.ts
CHANGED
|
@@ -7,14 +7,20 @@
|
|
|
7
7
|
* summary: "Station already has an unfinished wash",
|
|
8
8
|
* });
|
|
9
9
|
*
|
|
10
|
-
* //
|
|
11
|
-
*
|
|
10
|
+
* // Throw it with context — callable form, symmetric with defineEvent/defineAction:
|
|
11
|
+
* throw WashInProgress({ stationId: input.id });
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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):
|
|
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:
|
|
54
|
-
export declare const Forbidden:
|
|
55
|
-
export declare const NotFound:
|
|
56
|
-
export declare const Gone:
|
|
57
|
-
export declare const BadRequest:
|
|
58
|
-
export declare const Conflict:
|
|
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
|
|
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"}
|
package/dist/define-error.js
CHANGED
|
@@ -7,19 +7,26 @@
|
|
|
7
7
|
* summary: "Station already has an unfinished wash",
|
|
8
8
|
* });
|
|
9
9
|
*
|
|
10
|
-
* //
|
|
11
|
-
*
|
|
10
|
+
* // Throw it with context — callable form, symmetric with defineEvent/defineAction:
|
|
11
|
+
* throw WashInProgress({ stationId: input.id });
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/define-error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-error.js","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|