@nwire/express 0.10.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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/http-express.d.ts +73 -0
- package/dist/http-express.js +336 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @nwire/express
|
|
2
|
+
|
|
3
|
+
> Express-backed HTTP adopter — same wires as `@nwire/koa`, different runtime.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @nwire/express @nwire/app @nwire/endpoint @nwire/wires
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`express@^4 || ^5` is a peer dep.
|
|
10
|
+
|
|
11
|
+
## Two integration modes
|
|
12
|
+
|
|
13
|
+
### 1. Drive Nwire as the HTTP server (Adapter mode)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createApp } from "@nwire/app";
|
|
17
|
+
import { endpoint } from "@nwire/endpoint";
|
|
18
|
+
import { post } from "@nwire/wires/http";
|
|
19
|
+
import { expressAdapter } from "@nwire/express";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
const app = createApp({ appName: "api" });
|
|
23
|
+
app.wire(
|
|
24
|
+
post("/hello", { body: z.object({ name: z.string() }) }),
|
|
25
|
+
async (input) => ({ message: `Hello, ${input.name}!` }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await endpoint("api", { port: 3000 })
|
|
29
|
+
.use(expressAdapter({ prefix: "/api" }))
|
|
30
|
+
.mount(app)
|
|
31
|
+
.run();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Mount Nwire wires inside an existing Express app
|
|
35
|
+
|
|
36
|
+
When you already run Express (or NestJS, which uses Express under the
|
|
37
|
+
hood), `nwireToExpressRouter` extracts wired routes as an Express Router
|
|
38
|
+
you mount alongside legacy handlers. Migration without rewrite.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import express from "express";
|
|
42
|
+
import { createApp } from "@nwire/app";
|
|
43
|
+
import { nwireToExpressRouter } from "@nwire/express";
|
|
44
|
+
|
|
45
|
+
const legacy = express();
|
|
46
|
+
legacy.get("/legacy", legacyHandler);
|
|
47
|
+
|
|
48
|
+
const nwireApp = createApp({ appName: "orders" });
|
|
49
|
+
nwireApp.wire(/* ... */);
|
|
50
|
+
await nwireApp.start();
|
|
51
|
+
|
|
52
|
+
legacy.use("/api/nwire", nwireToExpressRouter(nwireApp));
|
|
53
|
+
legacy.listen(3000);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The router handles its own JSON body-parsing (`bodyParser: false` opts
|
|
57
|
+
out) so it works whether Express has body-parser globally or not. See
|
|
58
|
+
[`examples/nest-interop`](../../examples/nest-interop) for the full
|
|
59
|
+
Nwire-inside-NestJS pattern.
|
|
60
|
+
|
|
61
|
+
## Adopter config
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
expressAdapter({
|
|
65
|
+
port: 3000, // 0 = ephemeral; .port() returns the bound port
|
|
66
|
+
host: "0.0.0.0",
|
|
67
|
+
prefix: "/api",
|
|
68
|
+
middleware: [helmet(), morgan("combined")],
|
|
69
|
+
logger: myLogger,
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Bridging middleware
|
|
74
|
+
|
|
75
|
+
`fromExpressMiddleware(mw)` wraps an Express middleware so it can be
|
|
76
|
+
passed in `httpKoa({ middleware: [...] })` — useful when an Express-only
|
|
77
|
+
middleware (e.g., a vendored auth check) needs to run inside a Koa-backed
|
|
78
|
+
adopter.
|
|
79
|
+
|
|
80
|
+
## Related
|
|
81
|
+
|
|
82
|
+
- [`@nwire/koa`](../nwire-koa) — Koa-backed sibling adopter
|
|
83
|
+
- [`@nwire/endpoint`](../core-endpoint) — the lifecycle host
|
|
84
|
+
- [`examples/nest-interop`](../../examples/nest-interop) — Nwire inside NestJS
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/express` — Express-backed HTTP adopter.
|
|
3
|
+
*
|
|
4
|
+
* import { expressAdapter } from "@nwire/express";
|
|
5
|
+
*
|
|
6
|
+
* await endpoint("api", { port: 3000 })
|
|
7
|
+
* .use(expressAdapter({ prefix: "/api" }))
|
|
8
|
+
* .mount(app)
|
|
9
|
+
* .run();
|
|
10
|
+
*
|
|
11
|
+
* Consumes wires whose `binding.$adapter === "http"` — same contract as
|
|
12
|
+
* `httpKoa`. Pick one or the other based on which framework your team
|
|
13
|
+
* lives in; both run the same wires identically.
|
|
14
|
+
*
|
|
15
|
+
* Also exports `fromExpressMiddleware(mw)` — wraps an Express middleware
|
|
16
|
+
* function so it can be passed as `httpKoa({ middleware: [...] })`. Useful
|
|
17
|
+
* when adopting Nwire inside an existing Express monorepo where stock
|
|
18
|
+
* middleware (helmet, morgan, passport) is already in place.
|
|
19
|
+
*/
|
|
20
|
+
import express, { type Application, type NextFunction, type Request, type Response } from "express";
|
|
21
|
+
import type { Adapter } from "@nwire/endpoint";
|
|
22
|
+
import type { Container } from "@nwire/container";
|
|
23
|
+
import type { Wire } from "@nwire/wires";
|
|
24
|
+
import { type Logger } from "@nwire/logger";
|
|
25
|
+
export interface ExpressAdapterConfig {
|
|
26
|
+
/** Bound port. 0 = ephemeral. Default 3000. */
|
|
27
|
+
readonly port?: number;
|
|
28
|
+
/** Host bind address. Default 0.0.0.0. */
|
|
29
|
+
readonly host?: string;
|
|
30
|
+
/** Route prefix mounted under (e.g. "/api"). Default "/". */
|
|
31
|
+
readonly prefix?: string;
|
|
32
|
+
/** Adopter-wide Express middleware run before every wired handler. */
|
|
33
|
+
readonly middleware?: ReadonlyArray<(req: Request, res: Response, next: NextFunction) => void>;
|
|
34
|
+
/** Logger. Defaults to ConsoleLogger. */
|
|
35
|
+
readonly logger?: Logger;
|
|
36
|
+
}
|
|
37
|
+
export interface ExpressAdapter extends Adapter {
|
|
38
|
+
port(): number | undefined;
|
|
39
|
+
/** Underlying Express app — present after boot. Tests can pass this to
|
|
40
|
+
* supertest(adapter.app()!) for in-process request simulation. */
|
|
41
|
+
app(): Application | undefined;
|
|
42
|
+
}
|
|
43
|
+
export declare function expressAdapter(config?: ExpressAdapterConfig): ExpressAdapter;
|
|
44
|
+
/**
|
|
45
|
+
* Build an Express Router from a Nwire App's wires. Mount it on an
|
|
46
|
+
* existing Express server to add Nwire routes alongside legacy handlers:
|
|
47
|
+
*
|
|
48
|
+
* const nwireApp = buildNwireApp();
|
|
49
|
+
* await nwireApp.start();
|
|
50
|
+
* const router = nwireToExpressRouter(nwireApp);
|
|
51
|
+
* expressApp.use("/api/nwire", router);
|
|
52
|
+
*
|
|
53
|
+
* The router uses the same dispatch path as `expressAdapter` — same
|
|
54
|
+
* validation, error envelope, container scoping — but doesn't boot its
|
|
55
|
+
* own HTTP server.
|
|
56
|
+
*/
|
|
57
|
+
export declare function nwireToExpressRouter(app: {
|
|
58
|
+
interface: {
|
|
59
|
+
wires: ReadonlyArray<Wire>;
|
|
60
|
+
};
|
|
61
|
+
container: Container;
|
|
62
|
+
}, options?: {
|
|
63
|
+
logger?: Logger;
|
|
64
|
+
bodyParser?: boolean;
|
|
65
|
+
}): express.Router;
|
|
66
|
+
/**
|
|
67
|
+
* Wrap an Express middleware so it can be passed to `httpKoa({ middleware: [...] })`.
|
|
68
|
+
* Bridges the `(req, res, next)` Express contract into Koa's async chain.
|
|
69
|
+
*/
|
|
70
|
+
export declare function fromExpressMiddleware(mw: (req: Request, res: Response, next: NextFunction) => void): (kctx: {
|
|
71
|
+
req: unknown;
|
|
72
|
+
res: unknown;
|
|
73
|
+
}, next: () => Promise<unknown>) => Promise<void>;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/express` — Express-backed HTTP adopter.
|
|
3
|
+
*
|
|
4
|
+
* import { expressAdapter } from "@nwire/express";
|
|
5
|
+
*
|
|
6
|
+
* await endpoint("api", { port: 3000 })
|
|
7
|
+
* .use(expressAdapter({ prefix: "/api" }))
|
|
8
|
+
* .mount(app)
|
|
9
|
+
* .run();
|
|
10
|
+
*
|
|
11
|
+
* Consumes wires whose `binding.$adapter === "http"` — same contract as
|
|
12
|
+
* `httpKoa`. Pick one or the other based on which framework your team
|
|
13
|
+
* lives in; both run the same wires identically.
|
|
14
|
+
*
|
|
15
|
+
* Also exports `fromExpressMiddleware(mw)` — wraps an Express middleware
|
|
16
|
+
* function so it can be passed as `httpKoa({ middleware: [...] })`. Useful
|
|
17
|
+
* when adopting Nwire inside an existing Express monorepo where stock
|
|
18
|
+
* middleware (helmet, morgan, passport) is already in place.
|
|
19
|
+
*/
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import express from "express";
|
|
22
|
+
import { dummyContainer } from "@nwire/container";
|
|
23
|
+
import { ConsoleLogger } from "@nwire/logger";
|
|
24
|
+
function isHttpBinding(b) {
|
|
25
|
+
return (typeof b === "object" &&
|
|
26
|
+
b !== null &&
|
|
27
|
+
b.$adapter === "http" &&
|
|
28
|
+
typeof b.verb === "string" &&
|
|
29
|
+
typeof b.path === "string");
|
|
30
|
+
}
|
|
31
|
+
export function expressAdapter(config = {}) {
|
|
32
|
+
let server;
|
|
33
|
+
let appInstance;
|
|
34
|
+
const logger = config.logger ?? new ConsoleLogger();
|
|
35
|
+
return {
|
|
36
|
+
$kind: "adapter",
|
|
37
|
+
kind: "http",
|
|
38
|
+
port() {
|
|
39
|
+
if (!server)
|
|
40
|
+
return undefined;
|
|
41
|
+
const addr = server.address();
|
|
42
|
+
return typeof addr === "object" && addr !== null ? addr.port : undefined;
|
|
43
|
+
},
|
|
44
|
+
app() {
|
|
45
|
+
return appInstance;
|
|
46
|
+
},
|
|
47
|
+
async boot(ctx) {
|
|
48
|
+
const expressApp = express();
|
|
49
|
+
appInstance = expressApp;
|
|
50
|
+
expressApp.use(express.json());
|
|
51
|
+
for (const mw of config.middleware ?? []) {
|
|
52
|
+
expressApp.use(mw);
|
|
53
|
+
}
|
|
54
|
+
const prefix = config.prefix ?? "";
|
|
55
|
+
for (const wire of ctx.wires) {
|
|
56
|
+
if (!isHttpBinding(wire.binding))
|
|
57
|
+
continue;
|
|
58
|
+
const binding = wire.binding;
|
|
59
|
+
const verb = binding.verb;
|
|
60
|
+
const path = prefix + binding.path;
|
|
61
|
+
const handler = async (req, res) => {
|
|
62
|
+
let input = {};
|
|
63
|
+
try {
|
|
64
|
+
if (binding.params) {
|
|
65
|
+
Object.assign(input, binding.params.parse(req.params));
|
|
66
|
+
}
|
|
67
|
+
if (binding.body) {
|
|
68
|
+
Object.assign(input, binding.body.parse(req.body));
|
|
69
|
+
}
|
|
70
|
+
if (binding.query) {
|
|
71
|
+
Object.assign(input, binding.query.parse(req.query));
|
|
72
|
+
}
|
|
73
|
+
if (!binding.params && !binding.body && !binding.query) {
|
|
74
|
+
input = {
|
|
75
|
+
...req.params,
|
|
76
|
+
...req.body,
|
|
77
|
+
...req.query,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
res.status(400).json({
|
|
83
|
+
error: { code: "validation_failed", summary: err.message },
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const parentContainer = ctx.containerOf(wire) ?? dummyContainer();
|
|
88
|
+
const reqContainer = parentContainer.createScope();
|
|
89
|
+
const envelopePartial = {
|
|
90
|
+
tenant: req.headers["x-tenant"] ?? undefined,
|
|
91
|
+
userId: req.user?.id ?? undefined,
|
|
92
|
+
correlationId: req.headers["x-correlation-id"] ?? undefined,
|
|
93
|
+
causationId: req.headers["x-causation-id"] ?? undefined,
|
|
94
|
+
};
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
const wireApp = wire.app;
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
const runtimeExecute = wireApp?.runtime?.execute;
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const hasRunMethod = typeof wire.handler?.run === "function";
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
if (runtimeExecute && hasRunMethod) {
|
|
104
|
+
result = await runtimeExecute.call(wireApp.runtime, wire.handler, input, envelopePartial);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const fn = wire.handler.run ?? wire.handler;
|
|
109
|
+
const handlerCtx = {
|
|
110
|
+
resolve: (name) => reqContainer.resolve(name),
|
|
111
|
+
logger,
|
|
112
|
+
req,
|
|
113
|
+
res,
|
|
114
|
+
envelope: envelopePartial,
|
|
115
|
+
};
|
|
116
|
+
result = await fn(input, handlerCtx);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const e = err;
|
|
121
|
+
res.status(typeof e.status === "number" ? e.status : 500).json({
|
|
122
|
+
error: {
|
|
123
|
+
code: e.code ?? "internal_error",
|
|
124
|
+
summary: e.summary ?? e.message ?? "Internal error",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Response shaping: response-instance | $status envelope | verbatim.
|
|
130
|
+
if (result &&
|
|
131
|
+
typeof result === "object" &&
|
|
132
|
+
result.$kind === "response-instance") {
|
|
133
|
+
const env = result;
|
|
134
|
+
res.status(env.status ?? 200).json(env.body);
|
|
135
|
+
}
|
|
136
|
+
else if (result &&
|
|
137
|
+
typeof result === "object" &&
|
|
138
|
+
"$status" in result) {
|
|
139
|
+
const env = result;
|
|
140
|
+
res.status(env.$status ?? 200).json(env.body);
|
|
141
|
+
}
|
|
142
|
+
else if (result === undefined) {
|
|
143
|
+
res.status(204).end();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
res.status(200).json(result);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
switch (verb) {
|
|
150
|
+
case "get":
|
|
151
|
+
expressApp.get(path, handler);
|
|
152
|
+
break;
|
|
153
|
+
case "post":
|
|
154
|
+
expressApp.post(path, handler);
|
|
155
|
+
break;
|
|
156
|
+
case "put":
|
|
157
|
+
expressApp.put(path, handler);
|
|
158
|
+
break;
|
|
159
|
+
case "patch":
|
|
160
|
+
expressApp.patch(path, handler);
|
|
161
|
+
break;
|
|
162
|
+
case "delete":
|
|
163
|
+
expressApp.delete(path, handler);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Top-level error handler — catches throws from middleware.
|
|
168
|
+
expressApp.use((err, _req, res, _next) => {
|
|
169
|
+
const e = err;
|
|
170
|
+
res.status(typeof e.status === "number" ? e.status : 500).json({
|
|
171
|
+
error: {
|
|
172
|
+
code: e.code ?? "internal_error",
|
|
173
|
+
summary: e.summary ?? e.message ?? "Internal error",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
server = http.createServer(expressApp);
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
server.once("error", reject);
|
|
180
|
+
server.listen(config.port ?? 3000, config.host ?? "0.0.0.0", () => {
|
|
181
|
+
server.off("error", reject);
|
|
182
|
+
resolve();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
logger.info(`[express] listening on ${config.host ?? "0.0.0.0"}:${server.address().port}`);
|
|
186
|
+
ctx.addCheck({ name: "express", check: () => undefined });
|
|
187
|
+
},
|
|
188
|
+
async shutdown() {
|
|
189
|
+
if (server) {
|
|
190
|
+
await new Promise((resolve) => server.close(() => {
|
|
191
|
+
resolve();
|
|
192
|
+
}));
|
|
193
|
+
server = undefined;
|
|
194
|
+
}
|
|
195
|
+
appInstance = undefined;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// ─── Foreign integration: expose Nwire as Express middleware ──────
|
|
200
|
+
/**
|
|
201
|
+
* Build an Express Router from a Nwire App's wires. Mount it on an
|
|
202
|
+
* existing Express server to add Nwire routes alongside legacy handlers:
|
|
203
|
+
*
|
|
204
|
+
* const nwireApp = buildNwireApp();
|
|
205
|
+
* await nwireApp.start();
|
|
206
|
+
* const router = nwireToExpressRouter(nwireApp);
|
|
207
|
+
* expressApp.use("/api/nwire", router);
|
|
208
|
+
*
|
|
209
|
+
* The router uses the same dispatch path as `expressAdapter` — same
|
|
210
|
+
* validation, error envelope, container scoping — but doesn't boot its
|
|
211
|
+
* own HTTP server.
|
|
212
|
+
*/
|
|
213
|
+
export function nwireToExpressRouter(app, options = {}) {
|
|
214
|
+
const router = express.Router();
|
|
215
|
+
const logger = options.logger ?? new ConsoleLogger();
|
|
216
|
+
if (options.bodyParser !== false) {
|
|
217
|
+
router.use(express.json());
|
|
218
|
+
}
|
|
219
|
+
for (const wire of app.interface.wires) {
|
|
220
|
+
if (!isHttpBinding(wire.binding))
|
|
221
|
+
continue;
|
|
222
|
+
const binding = wire.binding;
|
|
223
|
+
const handler = async (req, res) => {
|
|
224
|
+
let input = {};
|
|
225
|
+
try {
|
|
226
|
+
if (binding.params)
|
|
227
|
+
Object.assign(input, binding.params.parse(req.params));
|
|
228
|
+
if (binding.body)
|
|
229
|
+
Object.assign(input, binding.body.parse(req.body));
|
|
230
|
+
if (binding.query)
|
|
231
|
+
Object.assign(input, binding.query.parse(req.query));
|
|
232
|
+
if (!binding.params && !binding.body && !binding.query) {
|
|
233
|
+
input = {
|
|
234
|
+
...req.params,
|
|
235
|
+
...req.body,
|
|
236
|
+
...req.query,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
res.status(400).json({
|
|
242
|
+
error: { code: "validation_failed", summary: err.message },
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const reqContainer = app.container.createScope();
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
248
|
+
const wireApp = wire.app ?? app;
|
|
249
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
250
|
+
const runtimeExecute = wireApp?.runtime?.execute;
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
252
|
+
const hasRunMethod = typeof wire.handler?.run === "function";
|
|
253
|
+
try {
|
|
254
|
+
let result;
|
|
255
|
+
if (runtimeExecute && hasRunMethod) {
|
|
256
|
+
result = await runtimeExecute.call(wireApp.runtime, wire.handler, input, {});
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const fn = wire.handler.run ?? wire.handler;
|
|
261
|
+
const ctx = {
|
|
262
|
+
resolve: (name) => reqContainer.resolve(name),
|
|
263
|
+
logger,
|
|
264
|
+
req,
|
|
265
|
+
res,
|
|
266
|
+
envelope: {},
|
|
267
|
+
};
|
|
268
|
+
result = await fn(input, ctx);
|
|
269
|
+
}
|
|
270
|
+
if (result &&
|
|
271
|
+
typeof result === "object" &&
|
|
272
|
+
result.$kind === "response-instance") {
|
|
273
|
+
const env = result;
|
|
274
|
+
res.status(env.status ?? 200).json(env.body);
|
|
275
|
+
}
|
|
276
|
+
else if (result &&
|
|
277
|
+
typeof result === "object" &&
|
|
278
|
+
"$status" in result) {
|
|
279
|
+
const env = result;
|
|
280
|
+
res.status(env.$status ?? 200).json(env.body);
|
|
281
|
+
}
|
|
282
|
+
else if (result === undefined) {
|
|
283
|
+
res.status(204).end();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
res.status(200).json(result);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
const e = err;
|
|
291
|
+
res.status(typeof e.status === "number" ? e.status : 500).json({
|
|
292
|
+
error: {
|
|
293
|
+
code: e.code ?? "internal_error",
|
|
294
|
+
summary: e.summary ?? e.message ?? "Internal error",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
switch (binding.verb) {
|
|
300
|
+
case "get":
|
|
301
|
+
router.get(binding.path, handler);
|
|
302
|
+
break;
|
|
303
|
+
case "post":
|
|
304
|
+
router.post(binding.path, handler);
|
|
305
|
+
break;
|
|
306
|
+
case "put":
|
|
307
|
+
router.put(binding.path, handler);
|
|
308
|
+
break;
|
|
309
|
+
case "patch":
|
|
310
|
+
router.patch(binding.path, handler);
|
|
311
|
+
break;
|
|
312
|
+
case "delete":
|
|
313
|
+
router.delete(binding.path, handler);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return router;
|
|
318
|
+
}
|
|
319
|
+
// ─── Koa <-> Express middleware bridge ─────────────────────────────
|
|
320
|
+
/**
|
|
321
|
+
* Wrap an Express middleware so it can be passed to `httpKoa({ middleware: [...] })`.
|
|
322
|
+
* Bridges the `(req, res, next)` Express contract into Koa's async chain.
|
|
323
|
+
*/
|
|
324
|
+
export function fromExpressMiddleware(mw) {
|
|
325
|
+
return async (kctx, next) => {
|
|
326
|
+
await new Promise((resolve, reject) => {
|
|
327
|
+
try {
|
|
328
|
+
mw(kctx.req, kctx.res, (err) => err ? reject(err) : resolve());
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
reject(err);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
await next();
|
|
335
|
+
};
|
|
336
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/express",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Nwire — Express-backed HTTP adopter. expressAdapter() consumes wires with binding.$adapter==='http' and mounts them on an Express server; fromExpressMiddleware() bridges Express middleware into httpKoa's middleware chain.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adopter",
|
|
7
|
+
"express",
|
|
8
|
+
"http",
|
|
9
|
+
"interop",
|
|
10
|
+
"nwire"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/http-express.js",
|
|
20
|
+
"types": "./dist/http-express.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/http-express.js",
|
|
24
|
+
"types": "./dist/http-express.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"express": "^5.1.0",
|
|
32
|
+
"@nwire/container": "0.10.0",
|
|
33
|
+
"@nwire/endpoint": "0.10.0",
|
|
34
|
+
"@nwire/wires": "0.10.0",
|
|
35
|
+
"@nwire/logger": "0.10.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/express": "^5.0.0",
|
|
39
|
+
"@types/node": "^22.19.9",
|
|
40
|
+
"@types/supertest": "^6.0.3",
|
|
41
|
+
"supertest": "^7.2.2",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.0.18",
|
|
44
|
+
"zod": "^4.0.0",
|
|
45
|
+
"@nwire/app": "0.10.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
52
|
+
"dev": "tsc --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|