@typokit/server-express 0.1.4
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/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/index.test.ts +297 -0
- package/src/index.ts +439 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { SerializerMap, TypoKitResponse, ValidatorMap, ValidationFieldError } from "@typokit/types";
|
|
3
|
+
import type { ServerAdapter } from "@typokit/core";
|
|
4
|
+
declare function validationErrorResponse(message: string, fields: ValidationFieldError[]): TypoKitResponse;
|
|
5
|
+
declare function runValidators(routeHandler: {
|
|
6
|
+
validators?: {
|
|
7
|
+
params?: string;
|
|
8
|
+
query?: string;
|
|
9
|
+
body?: string;
|
|
10
|
+
};
|
|
11
|
+
}, validatorMap: ValidatorMap | null, params: Record<string, string>, query: Record<string, string | string[] | undefined>, body: unknown): TypoKitResponse | undefined;
|
|
12
|
+
declare function serializeResponse(response: TypoKitResponse, serializerRef: string | undefined, serializerMap: SerializerMap | null): TypoKitResponse;
|
|
13
|
+
export interface ExpressServerOptions {
|
|
14
|
+
/** Pass an existing Express app instance instead of creating a new one */
|
|
15
|
+
app?: Express;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create an Express server adapter for TypoKit.
|
|
19
|
+
*
|
|
20
|
+
* Provides a migration path for teams with existing Express applications.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { expressServer } from "@typokit/server-express";
|
|
24
|
+
* const adapter = expressServer();
|
|
25
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
26
|
+
* const handle = await adapter.listen(3000);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function expressServer(options?: ExpressServerOptions): ServerAdapter;
|
|
30
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
31
|
+
export { type ServerAdapter } from "@typokit/core";
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAkC,MAAM,SAAS,CAAC;AAGvE,OAAO,KAAK,EAOV,aAAa,EAGb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,eAAe,CAAC;AA4DpE,iBAAS,uBAAuB,CAC9B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,oBAAoB,EAAE,GAC7B,eAAe,CAajB;AAED,iBAAS,aAAa,CACpB,YAAY,EAAE;IACZ,UAAU,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE,EACD,YAAY,EAAE,YAAY,GAAG,IAAI,EACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACpD,IAAI,EAAE,OAAO,GACZ,eAAe,GAAG,SAAS,CA4D7B;AAID,iBAAS,iBAAiB,CACxB,QAAQ,EAAE,eAAe,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,aAAa,EAAE,aAAa,GAAG,IAAI,GAClC,eAAe,CA8BjB;AAID,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAqM3E;AAGD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,uBAAuB,EAAE,CAAC;AACrE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// @typokit/server-express — Express Server Adapter
|
|
2
|
+
//
|
|
3
|
+
// Translates TypoKit's compiled route table into Express-native route
|
|
4
|
+
// registrations. Provides a migration path for teams with existing
|
|
5
|
+
// Express applications.
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
|
|
9
|
+
/**
|
|
10
|
+
* Recursively walk the compiled radix tree and collect all registered routes
|
|
11
|
+
* as flat entries with their full paths reconstructed.
|
|
12
|
+
*/
|
|
13
|
+
function collectRoutes(node, prefix, entries) {
|
|
14
|
+
if (node.handlers) {
|
|
15
|
+
for (const [method, handler] of Object.entries(node.handlers)) {
|
|
16
|
+
if (handler) {
|
|
17
|
+
entries.push({
|
|
18
|
+
method: method,
|
|
19
|
+
path: prefix || "/",
|
|
20
|
+
handlerRef: handler.ref,
|
|
21
|
+
validators: handler.validators,
|
|
22
|
+
serializer: handler.serializer,
|
|
23
|
+
middleware: handler.middleware,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Static children
|
|
29
|
+
if (node.children) {
|
|
30
|
+
for (const [segment, child] of Object.entries(node.children)) {
|
|
31
|
+
collectRoutes(child, `${prefix}/${segment}`, entries);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Param child (:id) — Express uses :param syntax same as TypoKit
|
|
35
|
+
if (node.paramChild) {
|
|
36
|
+
const paramNode = node.paramChild;
|
|
37
|
+
collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
|
|
38
|
+
}
|
|
39
|
+
// Wildcard child (*path)
|
|
40
|
+
if (node.wildcardChild) {
|
|
41
|
+
const wildcardNode = node.wildcardChild;
|
|
42
|
+
collectRoutes(wildcardNode, `${prefix}/*`, entries);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ─── Request Validation Pipeline ─────────────────────────────
|
|
46
|
+
function validationErrorResponse(message, fields) {
|
|
47
|
+
const body = {
|
|
48
|
+
error: {
|
|
49
|
+
code: "VALIDATION_ERROR",
|
|
50
|
+
message,
|
|
51
|
+
details: { fields },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
status: 400,
|
|
56
|
+
headers: { "content-type": "application/json" },
|
|
57
|
+
body,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function runValidators(routeHandler, validatorMap, params, query, body) {
|
|
61
|
+
if (!validatorMap || !routeHandler.validators) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const allErrors = [];
|
|
65
|
+
if (routeHandler.validators.params) {
|
|
66
|
+
const validator = validatorMap[routeHandler.validators.params];
|
|
67
|
+
if (validator) {
|
|
68
|
+
const result = validator(params);
|
|
69
|
+
if (!result.success && result.errors) {
|
|
70
|
+
for (const e of result.errors) {
|
|
71
|
+
allErrors.push({
|
|
72
|
+
path: `params.${e.path}`,
|
|
73
|
+
expected: e.expected,
|
|
74
|
+
actual: e.actual,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (routeHandler.validators.query) {
|
|
81
|
+
const validator = validatorMap[routeHandler.validators.query];
|
|
82
|
+
if (validator) {
|
|
83
|
+
const result = validator(query);
|
|
84
|
+
if (!result.success && result.errors) {
|
|
85
|
+
for (const e of result.errors) {
|
|
86
|
+
allErrors.push({
|
|
87
|
+
path: `query.${e.path}`,
|
|
88
|
+
expected: e.expected,
|
|
89
|
+
actual: e.actual,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (routeHandler.validators.body) {
|
|
96
|
+
const validator = validatorMap[routeHandler.validators.body];
|
|
97
|
+
if (validator) {
|
|
98
|
+
const result = validator(body);
|
|
99
|
+
if (!result.success && result.errors) {
|
|
100
|
+
for (const e of result.errors) {
|
|
101
|
+
allErrors.push({
|
|
102
|
+
path: `body.${e.path}`,
|
|
103
|
+
expected: e.expected,
|
|
104
|
+
actual: e.actual,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (allErrors.length > 0) {
|
|
111
|
+
return validationErrorResponse("Request validation failed", allErrors);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
// ─── Response Serialization Pipeline ──────────────────────────
|
|
116
|
+
function serializeResponse(response, serializerRef, serializerMap) {
|
|
117
|
+
if (response.body === null ||
|
|
118
|
+
response.body === undefined ||
|
|
119
|
+
typeof response.body === "string") {
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
const headers = { ...response.headers };
|
|
123
|
+
if (!headers["content-type"]) {
|
|
124
|
+
headers["content-type"] = "application/json";
|
|
125
|
+
}
|
|
126
|
+
if (serializerRef && serializerMap) {
|
|
127
|
+
const serializer = serializerMap[serializerRef];
|
|
128
|
+
if (serializer) {
|
|
129
|
+
return {
|
|
130
|
+
...response,
|
|
131
|
+
headers,
|
|
132
|
+
body: serializer(response.body),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
...response,
|
|
138
|
+
headers,
|
|
139
|
+
body: JSON.stringify(response.body),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create an Express server adapter for TypoKit.
|
|
144
|
+
*
|
|
145
|
+
* Provides a migration path for teams with existing Express applications.
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { expressServer } from "@typokit/server-express";
|
|
149
|
+
* const adapter = expressServer();
|
|
150
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
151
|
+
* const handle = await adapter.listen(3000);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function expressServer(options) {
|
|
155
|
+
const app = options?.app ?? express();
|
|
156
|
+
// Enable JSON body parsing
|
|
157
|
+
app.use(express.json());
|
|
158
|
+
const state = {
|
|
159
|
+
routeTable: null,
|
|
160
|
+
handlerMap: null,
|
|
161
|
+
middlewareChain: null,
|
|
162
|
+
validatorMap: null,
|
|
163
|
+
serializerMap: null,
|
|
164
|
+
};
|
|
165
|
+
/** Convert Express request to TypoKitRequest */
|
|
166
|
+
function normalizeRequest(raw) {
|
|
167
|
+
const req = raw;
|
|
168
|
+
const headers = {};
|
|
169
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
170
|
+
headers[key] = value;
|
|
171
|
+
}
|
|
172
|
+
const query = {};
|
|
173
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
174
|
+
if (typeof value === "string") {
|
|
175
|
+
query[key] = value;
|
|
176
|
+
}
|
|
177
|
+
else if (Array.isArray(value)) {
|
|
178
|
+
query[key] = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
method: req.method.toUpperCase(),
|
|
183
|
+
path: req.path,
|
|
184
|
+
headers,
|
|
185
|
+
body: req.body,
|
|
186
|
+
query,
|
|
187
|
+
params: req.params ?? {},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Write TypoKitResponse to Express response */
|
|
191
|
+
function writeResponse(raw, response) {
|
|
192
|
+
const res = raw;
|
|
193
|
+
// Set headers
|
|
194
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
195
|
+
if (value !== undefined) {
|
|
196
|
+
res.set(key, value);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
res.status(response.status);
|
|
200
|
+
if (response.body === null || response.body === undefined) {
|
|
201
|
+
res.end("");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
res.send(response.body);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const adapter = {
|
|
208
|
+
name: "express",
|
|
209
|
+
registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap, serializerMap) {
|
|
210
|
+
state.routeTable = routeTable;
|
|
211
|
+
state.handlerMap = handlerMap;
|
|
212
|
+
state.middlewareChain = middlewareChain;
|
|
213
|
+
state.validatorMap = validatorMap ?? null;
|
|
214
|
+
state.serializerMap = serializerMap ?? null;
|
|
215
|
+
// Collect all routes from the compiled radix tree
|
|
216
|
+
const routes = [];
|
|
217
|
+
collectRoutes(routeTable, "", routes);
|
|
218
|
+
// Register each route as an Express-native route
|
|
219
|
+
for (const route of routes) {
|
|
220
|
+
const method = route.method.toLowerCase();
|
|
221
|
+
app[method](route.path, async (req, res) => {
|
|
222
|
+
const typoReq = normalizeRequest(req);
|
|
223
|
+
// Run request validation pipeline
|
|
224
|
+
const validationError = runValidators({ validators: route.validators }, state.validatorMap, typoReq.params, typoReq.query, typoReq.body);
|
|
225
|
+
if (validationError) {
|
|
226
|
+
writeResponse(res, validationError);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const handlerFn = state.handlerMap[route.handlerRef];
|
|
230
|
+
if (!handlerFn) {
|
|
231
|
+
const errorResp = {
|
|
232
|
+
status: 500,
|
|
233
|
+
headers: { "content-type": "application/json" },
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
error: "Internal Server Error",
|
|
236
|
+
message: `Handler not found: ${route.handlerRef}`,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
writeResponse(res, errorResp);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Create request context and execute middleware chain
|
|
243
|
+
let ctx = createRequestContext();
|
|
244
|
+
if (state.middlewareChain &&
|
|
245
|
+
state.middlewareChain.entries.length > 0) {
|
|
246
|
+
const entries = state.middlewareChain.entries.map((e) => ({
|
|
247
|
+
name: e.name,
|
|
248
|
+
middleware: {
|
|
249
|
+
handler: async (input) => {
|
|
250
|
+
const mwReq = {
|
|
251
|
+
method: typoReq.method,
|
|
252
|
+
path: typoReq.path,
|
|
253
|
+
headers: input.headers,
|
|
254
|
+
body: input.body,
|
|
255
|
+
query: input.query,
|
|
256
|
+
params: input.params,
|
|
257
|
+
};
|
|
258
|
+
const response = await e.handler(mwReq, input.ctx, async () => {
|
|
259
|
+
return { status: 200, headers: {}, body: null };
|
|
260
|
+
});
|
|
261
|
+
return response;
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
}));
|
|
265
|
+
ctx = await executeMiddlewareChain(typoReq, ctx, entries);
|
|
266
|
+
}
|
|
267
|
+
// Call the handler
|
|
268
|
+
const response = await handlerFn(typoReq, ctx);
|
|
269
|
+
// Response serialization pipeline
|
|
270
|
+
const serialized = serializeResponse(response, route.serializer, state.serializerMap);
|
|
271
|
+
writeResponse(res, serialized);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
async listen(port) {
|
|
276
|
+
const server = createServer(app);
|
|
277
|
+
await new Promise((resolve) => {
|
|
278
|
+
server.listen(port, "0.0.0.0", () => resolve());
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
async close() {
|
|
282
|
+
await new Promise((resolve, reject) => {
|
|
283
|
+
server.close((err) => {
|
|
284
|
+
if (err)
|
|
285
|
+
reject(err);
|
|
286
|
+
else
|
|
287
|
+
resolve();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
_server: server,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
normalizeRequest,
|
|
295
|
+
writeResponse,
|
|
296
|
+
getNativeServer() {
|
|
297
|
+
return app;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
return adapter;
|
|
301
|
+
}
|
|
302
|
+
// Re-export for convenience
|
|
303
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
304
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typokit/server-express",
|
|
3
|
+
"exports": {
|
|
4
|
+
".": {
|
|
5
|
+
"import": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.4",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"express": "^5.1.0",
|
|
19
|
+
"@typokit/types": "0.1.4",
|
|
20
|
+
"@typokit/core": "0.1.4"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/express": "^5.0.0",
|
|
24
|
+
"@types/node": "^22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/KyleBastien/typokit",
|
|
29
|
+
"directory": "packages/server-express"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "rstest run --passWithNoTests"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// @typokit/server-express — Integration tests
|
|
2
|
+
import { describe, it, expect } from "@rstest/core";
|
|
3
|
+
import { expressServer } from "./index.js";
|
|
4
|
+
import type {
|
|
5
|
+
CompiledRouteTable,
|
|
6
|
+
HandlerMap,
|
|
7
|
+
MiddlewareChain,
|
|
8
|
+
ValidatorMap,
|
|
9
|
+
TypoKitRequest,
|
|
10
|
+
TypoKitResponse,
|
|
11
|
+
RequestContext,
|
|
12
|
+
ServerHandle,
|
|
13
|
+
} from "@typokit/types";
|
|
14
|
+
import type { Server } from "node:http";
|
|
15
|
+
|
|
16
|
+
// ─── Test Helpers ────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeRouteTable(
|
|
19
|
+
routes: {
|
|
20
|
+
method: string;
|
|
21
|
+
path: string;
|
|
22
|
+
ref: string;
|
|
23
|
+
validators?: { params?: string; query?: string; body?: string };
|
|
24
|
+
}[],
|
|
25
|
+
): CompiledRouteTable {
|
|
26
|
+
const root: CompiledRouteTable = {
|
|
27
|
+
segment: "",
|
|
28
|
+
children: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const route of routes) {
|
|
32
|
+
const segments = route.path.split("/").filter(Boolean);
|
|
33
|
+
let current: CompiledRouteTable = root;
|
|
34
|
+
|
|
35
|
+
for (const seg of segments) {
|
|
36
|
+
if (seg.startsWith(":")) {
|
|
37
|
+
if (!current.paramChild) {
|
|
38
|
+
current.paramChild = {
|
|
39
|
+
segment: seg.slice(1),
|
|
40
|
+
paramName: seg.slice(1),
|
|
41
|
+
children: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
current = current.paramChild;
|
|
45
|
+
} else {
|
|
46
|
+
if (!current.children![seg]) {
|
|
47
|
+
current.children![seg] = { segment: seg, children: {} };
|
|
48
|
+
}
|
|
49
|
+
current = current.children![seg];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!current.handlers) {
|
|
54
|
+
current.handlers = {};
|
|
55
|
+
}
|
|
56
|
+
(current.handlers as Record<string, unknown>)[route.method] = {
|
|
57
|
+
ref: route.ref,
|
|
58
|
+
validators: route.validators,
|
|
59
|
+
middleware: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeMiddlewareChain(): MiddlewareChain {
|
|
67
|
+
return { entries: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchJson(port: number, path: string, options?: RequestInit) {
|
|
71
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, options);
|
|
72
|
+
const text = await res.text();
|
|
73
|
+
let body: unknown;
|
|
74
|
+
try {
|
|
75
|
+
body = JSON.parse(text);
|
|
76
|
+
} catch {
|
|
77
|
+
body = text;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
status: res.status,
|
|
81
|
+
body,
|
|
82
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Tests ───────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe("expressServer", () => {
|
|
89
|
+
it("creates an adapter with correct name", () => {
|
|
90
|
+
const adapter = expressServer();
|
|
91
|
+
expect(adapter.name).toBe("express");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("getNativeServer returns Express app", () => {
|
|
95
|
+
const adapter = expressServer();
|
|
96
|
+
const app = adapter.getNativeServer!();
|
|
97
|
+
expect(app).toBeDefined();
|
|
98
|
+
expect(typeof (app as Record<string, unknown>).use).toBe("function");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("normalizeRequest converts Express request shape", () => {
|
|
102
|
+
const adapter = expressServer();
|
|
103
|
+
const mockReq = {
|
|
104
|
+
method: "GET",
|
|
105
|
+
path: "/test",
|
|
106
|
+
headers: { "content-type": "application/json" },
|
|
107
|
+
body: undefined,
|
|
108
|
+
query: { page: "1" },
|
|
109
|
+
params: { id: "42" },
|
|
110
|
+
};
|
|
111
|
+
const normalized = adapter.normalizeRequest(mockReq);
|
|
112
|
+
expect(normalized.method).toBe("GET");
|
|
113
|
+
expect(normalized.path).toBe("/test");
|
|
114
|
+
expect(normalized.params.id).toBe("42");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("expressServer integration", () => {
|
|
119
|
+
let handle: ServerHandle & { _server?: Server };
|
|
120
|
+
let port: number;
|
|
121
|
+
|
|
122
|
+
it("starts server, handles GET, and shuts down", async () => {
|
|
123
|
+
const routeTable = makeRouteTable([
|
|
124
|
+
{ method: "GET", path: "/health", ref: "getHealth" },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const handlerMap: HandlerMap = {
|
|
128
|
+
getHealth: async (
|
|
129
|
+
_req: TypoKitRequest,
|
|
130
|
+
_ctx: RequestContext,
|
|
131
|
+
): Promise<TypoKitResponse> => ({
|
|
132
|
+
status: 200,
|
|
133
|
+
headers: {},
|
|
134
|
+
body: { status: "ok" },
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const adapter = expressServer();
|
|
139
|
+
adapter.registerRoutes(routeTable, handlerMap, makeMiddlewareChain());
|
|
140
|
+
handle = (await adapter.listen(0)) as ServerHandle & { _server?: Server };
|
|
141
|
+
|
|
142
|
+
const addr = handle._server!.address();
|
|
143
|
+
port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
144
|
+
|
|
145
|
+
const res = await fetchJson(port, "/health");
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
expect((res.body as Record<string, unknown>).status).toBe("ok");
|
|
148
|
+
|
|
149
|
+
await handle.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("handles POST with JSON body", async () => {
|
|
153
|
+
const routeTable = makeRouteTable([
|
|
154
|
+
{ method: "POST", path: "/items", ref: "createItem" },
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const handlerMap: HandlerMap = {
|
|
158
|
+
createItem: async (
|
|
159
|
+
req: TypoKitRequest,
|
|
160
|
+
_ctx: RequestContext,
|
|
161
|
+
): Promise<TypoKitResponse> => ({
|
|
162
|
+
status: 201,
|
|
163
|
+
headers: {},
|
|
164
|
+
body: {
|
|
165
|
+
created: true,
|
|
166
|
+
name: (req.body as Record<string, unknown>)?.name,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const adapter = expressServer();
|
|
172
|
+
adapter.registerRoutes(routeTable, handlerMap, makeMiddlewareChain());
|
|
173
|
+
handle = (await adapter.listen(0)) as ServerHandle & { _server?: Server };
|
|
174
|
+
|
|
175
|
+
const addr = handle._server!.address();
|
|
176
|
+
port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
177
|
+
|
|
178
|
+
const res = await fetchJson(port, "/items", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
body: JSON.stringify({ name: "test-item" }),
|
|
182
|
+
});
|
|
183
|
+
expect(res.status).toBe(201);
|
|
184
|
+
expect((res.body as Record<string, unknown>).name).toBe("test-item");
|
|
185
|
+
|
|
186
|
+
await handle.close();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("handles parameterized routes", async () => {
|
|
190
|
+
const routeTable = makeRouteTable([
|
|
191
|
+
{ method: "GET", path: "/items/:id", ref: "getItem" },
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const handlerMap: HandlerMap = {
|
|
195
|
+
getItem: async (
|
|
196
|
+
req: TypoKitRequest,
|
|
197
|
+
_ctx: RequestContext,
|
|
198
|
+
): Promise<TypoKitResponse> => ({
|
|
199
|
+
status: 200,
|
|
200
|
+
headers: {},
|
|
201
|
+
body: { id: req.params.id },
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const adapter = expressServer();
|
|
206
|
+
adapter.registerRoutes(routeTable, handlerMap, makeMiddlewareChain());
|
|
207
|
+
handle = (await adapter.listen(0)) as ServerHandle & { _server?: Server };
|
|
208
|
+
|
|
209
|
+
const addr = handle._server!.address();
|
|
210
|
+
port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
211
|
+
|
|
212
|
+
const res = await fetchJson(port, "/items/abc123");
|
|
213
|
+
expect(res.status).toBe(200);
|
|
214
|
+
expect((res.body as Record<string, unknown>).id).toBe("abc123");
|
|
215
|
+
|
|
216
|
+
await handle.close();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("runs validation and returns 400 on failure", async () => {
|
|
220
|
+
const routeTable = makeRouteTable([
|
|
221
|
+
{
|
|
222
|
+
method: "POST",
|
|
223
|
+
path: "/validated",
|
|
224
|
+
ref: "validatedHandler",
|
|
225
|
+
validators: { body: "validateBody" },
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const handlerMap: HandlerMap = {
|
|
230
|
+
validatedHandler: async (
|
|
231
|
+
_req: TypoKitRequest,
|
|
232
|
+
_ctx: RequestContext,
|
|
233
|
+
): Promise<TypoKitResponse> => ({
|
|
234
|
+
status: 200,
|
|
235
|
+
headers: {},
|
|
236
|
+
body: { ok: true },
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const validatorMap: ValidatorMap = {
|
|
241
|
+
validateBody: (data: unknown) => {
|
|
242
|
+
const body = data as Record<string, unknown>;
|
|
243
|
+
if (!body || !body.name) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
errors: [{ path: "name", expected: "string", actual: "undefined" }],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { success: true };
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const adapter = expressServer();
|
|
254
|
+
adapter.registerRoutes(
|
|
255
|
+
routeTable,
|
|
256
|
+
handlerMap,
|
|
257
|
+
makeMiddlewareChain(),
|
|
258
|
+
validatorMap,
|
|
259
|
+
);
|
|
260
|
+
handle = (await adapter.listen(0)) as ServerHandle & { _server?: Server };
|
|
261
|
+
|
|
262
|
+
const addr = handle._server!.address();
|
|
263
|
+
port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
264
|
+
|
|
265
|
+
// Send invalid body
|
|
266
|
+
const res = await fetchJson(port, "/validated", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { "content-type": "application/json" },
|
|
269
|
+
body: JSON.stringify({}),
|
|
270
|
+
});
|
|
271
|
+
expect(res.status).toBe(400);
|
|
272
|
+
const body = res.body as { error: { code: string } };
|
|
273
|
+
expect(body.error.code).toBe("VALIDATION_ERROR");
|
|
274
|
+
|
|
275
|
+
await handle.close();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("returns 500 for missing handler", async () => {
|
|
279
|
+
const routeTable = makeRouteTable([
|
|
280
|
+
{ method: "GET", path: "/missing", ref: "nonExistentHandler" },
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
const handlerMap: HandlerMap = {};
|
|
284
|
+
|
|
285
|
+
const adapter = expressServer();
|
|
286
|
+
adapter.registerRoutes(routeTable, handlerMap, makeMiddlewareChain());
|
|
287
|
+
handle = (await adapter.listen(0)) as ServerHandle & { _server?: Server };
|
|
288
|
+
|
|
289
|
+
const addr = handle._server!.address();
|
|
290
|
+
port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
291
|
+
|
|
292
|
+
const res = await fetchJson(port, "/missing");
|
|
293
|
+
expect(res.status).toBe(500);
|
|
294
|
+
|
|
295
|
+
await handle.close();
|
|
296
|
+
});
|
|
297
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// @typokit/server-express — Express Server Adapter
|
|
2
|
+
//
|
|
3
|
+
// Translates TypoKit's compiled route table into Express-native route
|
|
4
|
+
// registrations. Provides a migration path for teams with existing
|
|
5
|
+
// Express applications.
|
|
6
|
+
|
|
7
|
+
import express from "express";
|
|
8
|
+
import type { Express, Request, Response, Application } from "express";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import type { Server } from "node:http";
|
|
11
|
+
import type {
|
|
12
|
+
CompiledRoute,
|
|
13
|
+
CompiledRouteTable,
|
|
14
|
+
ErrorResponse,
|
|
15
|
+
HandlerMap,
|
|
16
|
+
HttpMethod,
|
|
17
|
+
MiddlewareChain,
|
|
18
|
+
SerializerMap,
|
|
19
|
+
ServerHandle,
|
|
20
|
+
TypoKitRequest,
|
|
21
|
+
TypoKitResponse,
|
|
22
|
+
ValidatorMap,
|
|
23
|
+
ValidationFieldError,
|
|
24
|
+
} from "@typokit/types";
|
|
25
|
+
import type { ServerAdapter, MiddlewareEntry } from "@typokit/core";
|
|
26
|
+
import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
|
|
27
|
+
|
|
28
|
+
// ─── Route Traversal ─────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface RouteEntry {
|
|
31
|
+
method: HttpMethod;
|
|
32
|
+
path: string;
|
|
33
|
+
handlerRef: string;
|
|
34
|
+
validators?: { params?: string; query?: string; body?: string };
|
|
35
|
+
serializer?: string;
|
|
36
|
+
middleware: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Recursively walk the compiled radix tree and collect all registered routes
|
|
41
|
+
* as flat entries with their full paths reconstructed.
|
|
42
|
+
*/
|
|
43
|
+
function collectRoutes(
|
|
44
|
+
node: CompiledRoute,
|
|
45
|
+
prefix: string,
|
|
46
|
+
entries: RouteEntry[],
|
|
47
|
+
): void {
|
|
48
|
+
if (node.handlers) {
|
|
49
|
+
for (const [method, handler] of Object.entries(node.handlers)) {
|
|
50
|
+
if (handler) {
|
|
51
|
+
entries.push({
|
|
52
|
+
method: method as HttpMethod,
|
|
53
|
+
path: prefix || "/",
|
|
54
|
+
handlerRef: handler.ref,
|
|
55
|
+
validators: handler.validators,
|
|
56
|
+
serializer: handler.serializer,
|
|
57
|
+
middleware: handler.middleware,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Static children
|
|
64
|
+
if (node.children) {
|
|
65
|
+
for (const [segment, child] of Object.entries(node.children)) {
|
|
66
|
+
collectRoutes(child, `${prefix}/${segment}`, entries);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Param child (:id) — Express uses :param syntax same as TypoKit
|
|
71
|
+
if (node.paramChild) {
|
|
72
|
+
const paramNode = node.paramChild;
|
|
73
|
+
collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wildcard child (*path)
|
|
77
|
+
if (node.wildcardChild) {
|
|
78
|
+
const wildcardNode = node.wildcardChild;
|
|
79
|
+
collectRoutes(wildcardNode, `${prefix}/*`, entries);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Request Validation Pipeline ─────────────────────────────
|
|
84
|
+
|
|
85
|
+
function validationErrorResponse(
|
|
86
|
+
message: string,
|
|
87
|
+
fields: ValidationFieldError[],
|
|
88
|
+
): TypoKitResponse {
|
|
89
|
+
const body: ErrorResponse = {
|
|
90
|
+
error: {
|
|
91
|
+
code: "VALIDATION_ERROR",
|
|
92
|
+
message,
|
|
93
|
+
details: { fields },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
status: 400,
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
body,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runValidators(
|
|
104
|
+
routeHandler: {
|
|
105
|
+
validators?: { params?: string; query?: string; body?: string };
|
|
106
|
+
},
|
|
107
|
+
validatorMap: ValidatorMap | null,
|
|
108
|
+
params: Record<string, string>,
|
|
109
|
+
query: Record<string, string | string[] | undefined>,
|
|
110
|
+
body: unknown,
|
|
111
|
+
): TypoKitResponse | undefined {
|
|
112
|
+
if (!validatorMap || !routeHandler.validators) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const allErrors: ValidationFieldError[] = [];
|
|
117
|
+
|
|
118
|
+
if (routeHandler.validators.params) {
|
|
119
|
+
const validator = validatorMap[routeHandler.validators.params];
|
|
120
|
+
if (validator) {
|
|
121
|
+
const result = validator(params);
|
|
122
|
+
if (!result.success && result.errors) {
|
|
123
|
+
for (const e of result.errors) {
|
|
124
|
+
allErrors.push({
|
|
125
|
+
path: `params.${e.path}`,
|
|
126
|
+
expected: e.expected,
|
|
127
|
+
actual: e.actual,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (routeHandler.validators.query) {
|
|
135
|
+
const validator = validatorMap[routeHandler.validators.query];
|
|
136
|
+
if (validator) {
|
|
137
|
+
const result = validator(query);
|
|
138
|
+
if (!result.success && result.errors) {
|
|
139
|
+
for (const e of result.errors) {
|
|
140
|
+
allErrors.push({
|
|
141
|
+
path: `query.${e.path}`,
|
|
142
|
+
expected: e.expected,
|
|
143
|
+
actual: e.actual,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (routeHandler.validators.body) {
|
|
151
|
+
const validator = validatorMap[routeHandler.validators.body];
|
|
152
|
+
if (validator) {
|
|
153
|
+
const result = validator(body);
|
|
154
|
+
if (!result.success && result.errors) {
|
|
155
|
+
for (const e of result.errors) {
|
|
156
|
+
allErrors.push({
|
|
157
|
+
path: `body.${e.path}`,
|
|
158
|
+
expected: e.expected,
|
|
159
|
+
actual: e.actual,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (allErrors.length > 0) {
|
|
167
|
+
return validationErrorResponse("Request validation failed", allErrors);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Response Serialization Pipeline ──────────────────────────
|
|
174
|
+
|
|
175
|
+
function serializeResponse(
|
|
176
|
+
response: TypoKitResponse,
|
|
177
|
+
serializerRef: string | undefined,
|
|
178
|
+
serializerMap: SerializerMap | null,
|
|
179
|
+
): TypoKitResponse {
|
|
180
|
+
if (
|
|
181
|
+
response.body === null ||
|
|
182
|
+
response.body === undefined ||
|
|
183
|
+
typeof response.body === "string"
|
|
184
|
+
) {
|
|
185
|
+
return response;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headers = { ...response.headers };
|
|
189
|
+
if (!headers["content-type"]) {
|
|
190
|
+
headers["content-type"] = "application/json";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (serializerRef && serializerMap) {
|
|
194
|
+
const serializer = serializerMap[serializerRef];
|
|
195
|
+
if (serializer) {
|
|
196
|
+
return {
|
|
197
|
+
...response,
|
|
198
|
+
headers,
|
|
199
|
+
body: serializer(response.body),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...response,
|
|
206
|
+
headers,
|
|
207
|
+
body: JSON.stringify(response.body),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Express Server Adapter ──────────────────────────────────
|
|
212
|
+
|
|
213
|
+
export interface ExpressServerOptions {
|
|
214
|
+
/** Pass an existing Express app instance instead of creating a new one */
|
|
215
|
+
app?: Express;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface ExpressServerState {
|
|
219
|
+
routeTable: CompiledRouteTable | null;
|
|
220
|
+
handlerMap: HandlerMap | null;
|
|
221
|
+
middlewareChain: MiddlewareChain | null;
|
|
222
|
+
validatorMap: ValidatorMap | null;
|
|
223
|
+
serializerMap: SerializerMap | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create an Express server adapter for TypoKit.
|
|
228
|
+
*
|
|
229
|
+
* Provides a migration path for teams with existing Express applications.
|
|
230
|
+
*
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { expressServer } from "@typokit/server-express";
|
|
233
|
+
* const adapter = expressServer();
|
|
234
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
235
|
+
* const handle = await adapter.listen(3000);
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function expressServer(options?: ExpressServerOptions): ServerAdapter {
|
|
239
|
+
const app: Express = options?.app ?? express();
|
|
240
|
+
|
|
241
|
+
// Enable JSON body parsing
|
|
242
|
+
app.use(express.json());
|
|
243
|
+
|
|
244
|
+
const state: ExpressServerState = {
|
|
245
|
+
routeTable: null,
|
|
246
|
+
handlerMap: null,
|
|
247
|
+
middlewareChain: null,
|
|
248
|
+
validatorMap: null,
|
|
249
|
+
serializerMap: null,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Convert Express request to TypoKitRequest */
|
|
253
|
+
function normalizeRequest(raw: unknown): TypoKitRequest {
|
|
254
|
+
const req = raw as Request;
|
|
255
|
+
const headers: Record<string, string | string[] | undefined> = {};
|
|
256
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
257
|
+
headers[key] = value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const query: Record<string, string | string[] | undefined> = {};
|
|
261
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
262
|
+
if (typeof value === "string") {
|
|
263
|
+
query[key] = value;
|
|
264
|
+
} else if (Array.isArray(value)) {
|
|
265
|
+
query[key] = value as string[];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
method: req.method.toUpperCase() as HttpMethod,
|
|
271
|
+
path: req.path,
|
|
272
|
+
headers,
|
|
273
|
+
body: req.body as unknown,
|
|
274
|
+
query,
|
|
275
|
+
params: (req.params as Record<string, string>) ?? {},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Write TypoKitResponse to Express response */
|
|
280
|
+
function writeResponse(raw: unknown, response: TypoKitResponse): void {
|
|
281
|
+
const res = raw as Response;
|
|
282
|
+
|
|
283
|
+
// Set headers
|
|
284
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
285
|
+
if (value !== undefined) {
|
|
286
|
+
res.set(key, value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
res.status(response.status);
|
|
291
|
+
|
|
292
|
+
if (response.body === null || response.body === undefined) {
|
|
293
|
+
res.end("");
|
|
294
|
+
} else {
|
|
295
|
+
res.send(response.body);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const adapter: ServerAdapter = {
|
|
300
|
+
name: "express",
|
|
301
|
+
|
|
302
|
+
registerRoutes(
|
|
303
|
+
routeTable: CompiledRouteTable,
|
|
304
|
+
handlerMap: HandlerMap,
|
|
305
|
+
middlewareChain: MiddlewareChain,
|
|
306
|
+
validatorMap?: ValidatorMap,
|
|
307
|
+
serializerMap?: SerializerMap,
|
|
308
|
+
): void {
|
|
309
|
+
state.routeTable = routeTable;
|
|
310
|
+
state.handlerMap = handlerMap;
|
|
311
|
+
state.middlewareChain = middlewareChain;
|
|
312
|
+
state.validatorMap = validatorMap ?? null;
|
|
313
|
+
state.serializerMap = serializerMap ?? null;
|
|
314
|
+
|
|
315
|
+
// Collect all routes from the compiled radix tree
|
|
316
|
+
const routes: RouteEntry[] = [];
|
|
317
|
+
collectRoutes(routeTable, "", routes);
|
|
318
|
+
|
|
319
|
+
// Register each route as an Express-native route
|
|
320
|
+
for (const route of routes) {
|
|
321
|
+
const method = route.method.toLowerCase() as keyof Pick<
|
|
322
|
+
Application,
|
|
323
|
+
"get" | "post" | "put" | "delete" | "patch" | "options" | "head"
|
|
324
|
+
>;
|
|
325
|
+
|
|
326
|
+
app[method](route.path, async (req: Request, res: Response) => {
|
|
327
|
+
const typoReq = normalizeRequest(req);
|
|
328
|
+
|
|
329
|
+
// Run request validation pipeline
|
|
330
|
+
const validationError = runValidators(
|
|
331
|
+
{ validators: route.validators },
|
|
332
|
+
state.validatorMap,
|
|
333
|
+
typoReq.params,
|
|
334
|
+
typoReq.query,
|
|
335
|
+
typoReq.body,
|
|
336
|
+
);
|
|
337
|
+
if (validationError) {
|
|
338
|
+
writeResponse(res, validationError);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const handlerFn = state.handlerMap![route.handlerRef];
|
|
343
|
+
if (!handlerFn) {
|
|
344
|
+
const errorResp: TypoKitResponse = {
|
|
345
|
+
status: 500,
|
|
346
|
+
headers: { "content-type": "application/json" },
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
error: "Internal Server Error",
|
|
349
|
+
message: `Handler not found: ${route.handlerRef}`,
|
|
350
|
+
}),
|
|
351
|
+
};
|
|
352
|
+
writeResponse(res, errorResp);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Create request context and execute middleware chain
|
|
357
|
+
let ctx = createRequestContext();
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
state.middlewareChain &&
|
|
361
|
+
state.middlewareChain.entries.length > 0
|
|
362
|
+
) {
|
|
363
|
+
const entries: MiddlewareEntry[] =
|
|
364
|
+
state.middlewareChain.entries.map((e) => ({
|
|
365
|
+
name: e.name,
|
|
366
|
+
middleware: {
|
|
367
|
+
handler: async (input) => {
|
|
368
|
+
const mwReq: TypoKitRequest = {
|
|
369
|
+
method: typoReq.method,
|
|
370
|
+
path: typoReq.path,
|
|
371
|
+
headers: input.headers,
|
|
372
|
+
body: input.body,
|
|
373
|
+
query: input.query,
|
|
374
|
+
params: input.params,
|
|
375
|
+
};
|
|
376
|
+
const response = await e.handler(
|
|
377
|
+
mwReq,
|
|
378
|
+
input.ctx,
|
|
379
|
+
async () => {
|
|
380
|
+
return { status: 200, headers: {}, body: null };
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
return response as unknown as Record<string, unknown>;
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
ctx = await executeMiddlewareChain(typoReq, ctx, entries);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Call the handler
|
|
392
|
+
const response = await handlerFn(typoReq, ctx);
|
|
393
|
+
|
|
394
|
+
// Response serialization pipeline
|
|
395
|
+
const serialized = serializeResponse(
|
|
396
|
+
response,
|
|
397
|
+
route.serializer,
|
|
398
|
+
state.serializerMap,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
writeResponse(res, serialized);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
async listen(port: number): Promise<ServerHandle> {
|
|
407
|
+
const server: Server = createServer(app);
|
|
408
|
+
|
|
409
|
+
await new Promise<void>((resolve) => {
|
|
410
|
+
server.listen(port, "0.0.0.0", () => resolve());
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
async close(): Promise<void> {
|
|
415
|
+
await new Promise<void>((resolve, reject) => {
|
|
416
|
+
server.close((err?: Error) => {
|
|
417
|
+
if (err) reject(err);
|
|
418
|
+
else resolve();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
_server: server,
|
|
423
|
+
} as ServerHandle & { _server: Server };
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
normalizeRequest,
|
|
427
|
+
writeResponse,
|
|
428
|
+
|
|
429
|
+
getNativeServer(): unknown {
|
|
430
|
+
return app;
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
return adapter;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Re-export for convenience
|
|
438
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
439
|
+
export { type ServerAdapter } from "@typokit/core";
|