@typokit/server-fastify 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 +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
- package/src/index.test.ts +427 -0
- package/src/index.ts +418 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FastifyServerOptions } from "fastify";
|
|
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
|
+
/**
|
|
14
|
+
* Create a Fastify server adapter for TypoKit.
|
|
15
|
+
*
|
|
16
|
+
* Options are passed directly to the Fastify constructor (logger, trustProxy, etc.).
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { fastifyServer } from "@typokit/server-fastify";
|
|
20
|
+
* const adapter = fastifyServer({ logger: true, trustProxy: true });
|
|
21
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
22
|
+
* const handle = await adapter.listen(3000);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function fastifyServer(options?: FastifyServerOptions): ServerAdapter;
|
|
26
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
27
|
+
export { type ServerAdapter } from "@typokit/core";
|
|
28
|
+
//# 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,EAIV,oBAAoB,EACrB,MAAM,SAAS,CAAC;AACjB,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;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAkL3E;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,285 @@
|
|
|
1
|
+
// @typokit/server-fastify — Fastify Server Adapter
|
|
2
|
+
//
|
|
3
|
+
// Translates TypoKit's compiled route table into Fastify-native route
|
|
4
|
+
// registrations. Fastify-native middleware runs before TypoKit middleware
|
|
5
|
+
// per the architecture (Section 6.3).
|
|
6
|
+
import Fastify from "fastify";
|
|
7
|
+
import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
|
|
8
|
+
/**
|
|
9
|
+
* Recursively walk the compiled radix tree and collect all registered routes
|
|
10
|
+
* as flat entries with their full paths reconstructed.
|
|
11
|
+
*/
|
|
12
|
+
function collectRoutes(node, prefix, entries) {
|
|
13
|
+
if (node.handlers) {
|
|
14
|
+
for (const [method, handler] of Object.entries(node.handlers)) {
|
|
15
|
+
if (handler) {
|
|
16
|
+
entries.push({
|
|
17
|
+
method: method,
|
|
18
|
+
path: prefix || "/",
|
|
19
|
+
handlerRef: handler.ref,
|
|
20
|
+
validators: handler.validators,
|
|
21
|
+
serializer: handler.serializer,
|
|
22
|
+
middleware: handler.middleware,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Static children
|
|
28
|
+
if (node.children) {
|
|
29
|
+
for (const [segment, child] of Object.entries(node.children)) {
|
|
30
|
+
collectRoutes(child, `${prefix}/${segment}`, entries);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Param child (:id)
|
|
34
|
+
if (node.paramChild) {
|
|
35
|
+
const paramNode = node.paramChild;
|
|
36
|
+
collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
|
|
37
|
+
}
|
|
38
|
+
// Wildcard child (*path)
|
|
39
|
+
if (node.wildcardChild) {
|
|
40
|
+
const wildcardNode = node.wildcardChild;
|
|
41
|
+
collectRoutes(wildcardNode, `${prefix}/*`, entries);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ─── Request Validation Pipeline ─────────────────────────────
|
|
45
|
+
function validationErrorResponse(message, fields) {
|
|
46
|
+
const body = {
|
|
47
|
+
error: {
|
|
48
|
+
code: "VALIDATION_ERROR",
|
|
49
|
+
message,
|
|
50
|
+
details: { fields },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
status: 400,
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function runValidators(routeHandler, validatorMap, params, query, body) {
|
|
60
|
+
if (!validatorMap || !routeHandler.validators) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const allErrors = [];
|
|
64
|
+
if (routeHandler.validators.params) {
|
|
65
|
+
const validator = validatorMap[routeHandler.validators.params];
|
|
66
|
+
if (validator) {
|
|
67
|
+
const result = validator(params);
|
|
68
|
+
if (!result.success && result.errors) {
|
|
69
|
+
for (const e of result.errors) {
|
|
70
|
+
allErrors.push({
|
|
71
|
+
path: `params.${e.path}`,
|
|
72
|
+
expected: e.expected,
|
|
73
|
+
actual: e.actual,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (routeHandler.validators.query) {
|
|
80
|
+
const validator = validatorMap[routeHandler.validators.query];
|
|
81
|
+
if (validator) {
|
|
82
|
+
const result = validator(query);
|
|
83
|
+
if (!result.success && result.errors) {
|
|
84
|
+
for (const e of result.errors) {
|
|
85
|
+
allErrors.push({
|
|
86
|
+
path: `query.${e.path}`,
|
|
87
|
+
expected: e.expected,
|
|
88
|
+
actual: e.actual,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (routeHandler.validators.body) {
|
|
95
|
+
const validator = validatorMap[routeHandler.validators.body];
|
|
96
|
+
if (validator) {
|
|
97
|
+
const result = validator(body);
|
|
98
|
+
if (!result.success && result.errors) {
|
|
99
|
+
for (const e of result.errors) {
|
|
100
|
+
allErrors.push({
|
|
101
|
+
path: `body.${e.path}`,
|
|
102
|
+
expected: e.expected,
|
|
103
|
+
actual: e.actual,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (allErrors.length > 0) {
|
|
110
|
+
return validationErrorResponse("Request validation failed", allErrors);
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// ─── Response Serialization Pipeline ──────────────────────────
|
|
115
|
+
function serializeResponse(response, serializerRef, serializerMap) {
|
|
116
|
+
if (response.body === null ||
|
|
117
|
+
response.body === undefined ||
|
|
118
|
+
typeof response.body === "string") {
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
const headers = { ...response.headers };
|
|
122
|
+
if (!headers["content-type"]) {
|
|
123
|
+
headers["content-type"] = "application/json";
|
|
124
|
+
}
|
|
125
|
+
if (serializerRef && serializerMap) {
|
|
126
|
+
const serializer = serializerMap[serializerRef];
|
|
127
|
+
if (serializer) {
|
|
128
|
+
return {
|
|
129
|
+
...response,
|
|
130
|
+
headers,
|
|
131
|
+
body: serializer(response.body),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...response,
|
|
137
|
+
headers,
|
|
138
|
+
body: JSON.stringify(response.body),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Create a Fastify server adapter for TypoKit.
|
|
143
|
+
*
|
|
144
|
+
* Options are passed directly to the Fastify constructor (logger, trustProxy, etc.).
|
|
145
|
+
*
|
|
146
|
+
* ```ts
|
|
147
|
+
* import { fastifyServer } from "@typokit/server-fastify";
|
|
148
|
+
* const adapter = fastifyServer({ logger: true, trustProxy: true });
|
|
149
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
150
|
+
* const handle = await adapter.listen(3000);
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function fastifyServer(options) {
|
|
154
|
+
const app = Fastify(options ?? {});
|
|
155
|
+
const state = {
|
|
156
|
+
routeTable: null,
|
|
157
|
+
handlerMap: null,
|
|
158
|
+
middlewareChain: null,
|
|
159
|
+
validatorMap: null,
|
|
160
|
+
serializerMap: null,
|
|
161
|
+
};
|
|
162
|
+
/** Convert Fastify request to TypoKitRequest */
|
|
163
|
+
function normalizeRequest(raw) {
|
|
164
|
+
const req = raw;
|
|
165
|
+
const headers = {};
|
|
166
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
167
|
+
headers[key] = value;
|
|
168
|
+
}
|
|
169
|
+
const rawQuery = req.query;
|
|
170
|
+
return {
|
|
171
|
+
method: req.method.toUpperCase(),
|
|
172
|
+
path: req.url.split("?")[0],
|
|
173
|
+
headers,
|
|
174
|
+
body: req.body,
|
|
175
|
+
query: rawQuery ?? {},
|
|
176
|
+
params: req.params ?? {},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Write TypoKitResponse to Fastify reply */
|
|
180
|
+
function writeResponse(raw, response) {
|
|
181
|
+
const reply = raw;
|
|
182
|
+
// Set headers
|
|
183
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
184
|
+
if (value !== undefined) {
|
|
185
|
+
reply.header(key, value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
reply.status(response.status);
|
|
189
|
+
if (response.body === null || response.body === undefined) {
|
|
190
|
+
reply.send("");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
reply.send(response.body);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const adapter = {
|
|
197
|
+
name: "fastify",
|
|
198
|
+
registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap, serializerMap) {
|
|
199
|
+
state.routeTable = routeTable;
|
|
200
|
+
state.handlerMap = handlerMap;
|
|
201
|
+
state.middlewareChain = middlewareChain;
|
|
202
|
+
state.validatorMap = validatorMap ?? null;
|
|
203
|
+
state.serializerMap = serializerMap ?? null;
|
|
204
|
+
// Collect all routes from the compiled radix tree
|
|
205
|
+
const routes = [];
|
|
206
|
+
collectRoutes(routeTable, "", routes);
|
|
207
|
+
// Register each route as a Fastify-native route
|
|
208
|
+
for (const route of routes) {
|
|
209
|
+
app.route({
|
|
210
|
+
method: route.method,
|
|
211
|
+
url: route.path,
|
|
212
|
+
handler: async (req, reply) => {
|
|
213
|
+
const typoReq = normalizeRequest(req);
|
|
214
|
+
// Run request validation pipeline
|
|
215
|
+
const validationError = runValidators({ validators: route.validators }, state.validatorMap, typoReq.params, typoReq.query, typoReq.body);
|
|
216
|
+
if (validationError) {
|
|
217
|
+
writeResponse(reply, validationError);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const handlerFn = state.handlerMap[route.handlerRef];
|
|
221
|
+
if (!handlerFn) {
|
|
222
|
+
const errorResp = {
|
|
223
|
+
status: 500,
|
|
224
|
+
headers: { "content-type": "application/json" },
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
error: "Internal Server Error",
|
|
227
|
+
message: `Handler not found: ${route.handlerRef}`,
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
writeResponse(reply, errorResp);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Create request context and execute middleware chain
|
|
234
|
+
let ctx = createRequestContext();
|
|
235
|
+
if (state.middlewareChain &&
|
|
236
|
+
state.middlewareChain.entries.length > 0) {
|
|
237
|
+
const entries = state.middlewareChain.entries.map((e) => ({
|
|
238
|
+
name: e.name,
|
|
239
|
+
middleware: {
|
|
240
|
+
handler: async (input) => {
|
|
241
|
+
const mwReq = {
|
|
242
|
+
method: typoReq.method,
|
|
243
|
+
path: typoReq.path,
|
|
244
|
+
headers: input.headers,
|
|
245
|
+
body: input.body,
|
|
246
|
+
query: input.query,
|
|
247
|
+
params: input.params,
|
|
248
|
+
};
|
|
249
|
+
const response = await e.handler(mwReq, input.ctx, async () => {
|
|
250
|
+
return { status: 200, headers: {}, body: null };
|
|
251
|
+
});
|
|
252
|
+
return response;
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
256
|
+
ctx = await executeMiddlewareChain(typoReq, ctx, entries);
|
|
257
|
+
}
|
|
258
|
+
// Call the handler
|
|
259
|
+
const response = await handlerFn(typoReq, ctx);
|
|
260
|
+
// Response serialization pipeline
|
|
261
|
+
const serialized = serializeResponse(response, route.serializer, state.serializerMap);
|
|
262
|
+
writeResponse(reply, serialized);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
async listen(port) {
|
|
268
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
269
|
+
return {
|
|
270
|
+
async close() {
|
|
271
|
+
await app.close();
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
normalizeRequest,
|
|
276
|
+
writeResponse,
|
|
277
|
+
getNativeServer() {
|
|
278
|
+
return app;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
return adapter;
|
|
282
|
+
}
|
|
283
|
+
// Re-export for convenience
|
|
284
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
285
|
+
//# 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,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typokit/server-fastify",
|
|
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
|
+
"fastify": "^5.3.3",
|
|
19
|
+
"@typokit/types": "0.1.4",
|
|
20
|
+
"@typokit/core": "0.1.4"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/KyleBastien/typokit",
|
|
28
|
+
"directory": "packages/server-fastify"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "rstest run --passWithNoTests"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// @typokit/server-fastify — Integration Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import { fastifyServer } from "./index.js";
|
|
5
|
+
import type {
|
|
6
|
+
CompiledRouteTable,
|
|
7
|
+
HandlerMap,
|
|
8
|
+
MiddlewareChain,
|
|
9
|
+
TypoKitRequest,
|
|
10
|
+
ValidatorMap,
|
|
11
|
+
RequestContext,
|
|
12
|
+
} from "@typokit/types";
|
|
13
|
+
import type { FastifyInstance } from "fastify";
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function makeRouteTable(
|
|
18
|
+
overrides?: Partial<CompiledRouteTable>,
|
|
19
|
+
): CompiledRouteTable {
|
|
20
|
+
return {
|
|
21
|
+
segment: "",
|
|
22
|
+
children: {
|
|
23
|
+
users: {
|
|
24
|
+
segment: "users",
|
|
25
|
+
handlers: {
|
|
26
|
+
GET: { ref: "users#list", middleware: [] },
|
|
27
|
+
POST: { ref: "users#create", middleware: [] },
|
|
28
|
+
},
|
|
29
|
+
paramChild: {
|
|
30
|
+
segment: ":id",
|
|
31
|
+
paramName: "id",
|
|
32
|
+
handlers: {
|
|
33
|
+
GET: { ref: "users#get", middleware: [] },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
health: {
|
|
38
|
+
segment: "health",
|
|
39
|
+
handlers: {
|
|
40
|
+
GET: { ref: "health#check", middleware: [] },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeHandlerMap(): HandlerMap {
|
|
49
|
+
return {
|
|
50
|
+
"users#list": async () => ({
|
|
51
|
+
status: 200,
|
|
52
|
+
headers: {},
|
|
53
|
+
body: [
|
|
54
|
+
{ id: 1, name: "Alice" },
|
|
55
|
+
{ id: 2, name: "Bob" },
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
"users#create": async (req: TypoKitRequest) => ({
|
|
59
|
+
status: 201,
|
|
60
|
+
headers: {},
|
|
61
|
+
body: {
|
|
62
|
+
id: 3,
|
|
63
|
+
name: (req.body as Record<string, unknown>)?.name ?? "Unknown",
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
"users#get": async (req: TypoKitRequest) => ({
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: {},
|
|
69
|
+
body: { id: req.params.id, name: "User " + req.params.id },
|
|
70
|
+
}),
|
|
71
|
+
"health#check": async () => ({
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: {},
|
|
74
|
+
body: { status: "ok" },
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emptyMiddlewareChain(): MiddlewareChain {
|
|
80
|
+
return { entries: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Helper to make HTTP requests to the Fastify-adapted server
|
|
84
|
+
async function fetchJson(
|
|
85
|
+
port: number,
|
|
86
|
+
path: string,
|
|
87
|
+
options?: RequestInit,
|
|
88
|
+
): Promise<{ status: number; body: unknown }> {
|
|
89
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
90
|
+
...options,
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
...(options?.headers as Record<string, string> | undefined),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const text = await res.text();
|
|
97
|
+
let body: unknown;
|
|
98
|
+
try {
|
|
99
|
+
body = JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
body = text;
|
|
102
|
+
}
|
|
103
|
+
return { status: res.status, body };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Tests ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("fastifyServer", () => {
|
|
109
|
+
it("implements ServerAdapter interface with correct name", () => {
|
|
110
|
+
const adapter = fastifyServer();
|
|
111
|
+
expect(adapter.name).toBe("fastify");
|
|
112
|
+
expect(typeof adapter.registerRoutes).toBe("function");
|
|
113
|
+
expect(typeof adapter.listen).toBe("function");
|
|
114
|
+
expect(typeof adapter.normalizeRequest).toBe("function");
|
|
115
|
+
expect(typeof adapter.writeResponse).toBe("function");
|
|
116
|
+
expect(typeof adapter.getNativeServer).toBe("function");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("getNativeServer returns the Fastify instance", () => {
|
|
120
|
+
const adapter = fastifyServer();
|
|
121
|
+
const native = adapter.getNativeServer!();
|
|
122
|
+
expect(native).toBeDefined();
|
|
123
|
+
// Fastify instances have a .route method
|
|
124
|
+
expect(typeof (native as Record<string, unknown>).route).toBe("function");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("routes GET /health correctly", async () => {
|
|
128
|
+
const adapter = fastifyServer({ logger: false });
|
|
129
|
+
adapter.registerRoutes(
|
|
130
|
+
makeRouteTable(),
|
|
131
|
+
makeHandlerMap(),
|
|
132
|
+
emptyMiddlewareChain(),
|
|
133
|
+
);
|
|
134
|
+
const handle = await adapter.listen(0);
|
|
135
|
+
try {
|
|
136
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
137
|
+
const addr = native.addresses()[0];
|
|
138
|
+
const port = addr.port;
|
|
139
|
+
|
|
140
|
+
const { status, body } = await fetchJson(port, "/health");
|
|
141
|
+
expect(status).toBe(200);
|
|
142
|
+
expect((body as Record<string, unknown>).status).toBe("ok");
|
|
143
|
+
} finally {
|
|
144
|
+
await handle.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("routes GET /users and returns list", async () => {
|
|
149
|
+
const adapter = fastifyServer({ logger: false });
|
|
150
|
+
adapter.registerRoutes(
|
|
151
|
+
makeRouteTable(),
|
|
152
|
+
makeHandlerMap(),
|
|
153
|
+
emptyMiddlewareChain(),
|
|
154
|
+
);
|
|
155
|
+
const handle = await adapter.listen(0);
|
|
156
|
+
try {
|
|
157
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
158
|
+
const port = native.addresses()[0].port;
|
|
159
|
+
|
|
160
|
+
const { status, body } = await fetchJson(port, "/users");
|
|
161
|
+
expect(status).toBe(200);
|
|
162
|
+
expect(Array.isArray(body)).toBe(true);
|
|
163
|
+
expect((body as Array<unknown>).length).toBe(2);
|
|
164
|
+
} finally {
|
|
165
|
+
await handle.close();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("routes POST /users with body", async () => {
|
|
170
|
+
const adapter = fastifyServer({ logger: false });
|
|
171
|
+
adapter.registerRoutes(
|
|
172
|
+
makeRouteTable(),
|
|
173
|
+
makeHandlerMap(),
|
|
174
|
+
emptyMiddlewareChain(),
|
|
175
|
+
);
|
|
176
|
+
const handle = await adapter.listen(0);
|
|
177
|
+
try {
|
|
178
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
179
|
+
const port = native.addresses()[0].port;
|
|
180
|
+
|
|
181
|
+
const { status, body } = await fetchJson(port, "/users", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
body: JSON.stringify({ name: "Charlie" }),
|
|
184
|
+
});
|
|
185
|
+
expect(status).toBe(201);
|
|
186
|
+
expect((body as Record<string, unknown>).name).toBe("Charlie");
|
|
187
|
+
} finally {
|
|
188
|
+
await handle.close();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("routes GET /users/:id with params", async () => {
|
|
193
|
+
const adapter = fastifyServer({ logger: false });
|
|
194
|
+
adapter.registerRoutes(
|
|
195
|
+
makeRouteTable(),
|
|
196
|
+
makeHandlerMap(),
|
|
197
|
+
emptyMiddlewareChain(),
|
|
198
|
+
);
|
|
199
|
+
const handle = await adapter.listen(0);
|
|
200
|
+
try {
|
|
201
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
202
|
+
const port = native.addresses()[0].port;
|
|
203
|
+
|
|
204
|
+
const { status, body } = await fetchJson(port, "/users/42");
|
|
205
|
+
expect(status).toBe(200);
|
|
206
|
+
expect((body as Record<string, unknown>).id).toBe("42");
|
|
207
|
+
expect((body as Record<string, unknown>).name).toBe("User 42");
|
|
208
|
+
} finally {
|
|
209
|
+
await handle.close();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns 404 for unknown routes", async () => {
|
|
214
|
+
const adapter = fastifyServer({ logger: false });
|
|
215
|
+
adapter.registerRoutes(
|
|
216
|
+
makeRouteTable(),
|
|
217
|
+
makeHandlerMap(),
|
|
218
|
+
emptyMiddlewareChain(),
|
|
219
|
+
);
|
|
220
|
+
const handle = await adapter.listen(0);
|
|
221
|
+
try {
|
|
222
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
223
|
+
const port = native.addresses()[0].port;
|
|
224
|
+
|
|
225
|
+
const res = await fetch(`http://127.0.0.1:${port}/nonexistent`);
|
|
226
|
+
expect(res.status).toBe(404);
|
|
227
|
+
} finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("runs request validation and returns 400 on failure", async () => {
|
|
233
|
+
const routeTable: CompiledRouteTable = {
|
|
234
|
+
segment: "",
|
|
235
|
+
children: {
|
|
236
|
+
items: {
|
|
237
|
+
segment: "items",
|
|
238
|
+
handlers: {
|
|
239
|
+
POST: {
|
|
240
|
+
ref: "items#create",
|
|
241
|
+
middleware: [],
|
|
242
|
+
validators: { body: "items#create.body" },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handlerMap: HandlerMap = {
|
|
250
|
+
"items#create": async (req: TypoKitRequest) => ({
|
|
251
|
+
status: 201,
|
|
252
|
+
headers: {},
|
|
253
|
+
body: req.body,
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const validatorMap: ValidatorMap = {
|
|
258
|
+
"items#create.body": (input: unknown) => {
|
|
259
|
+
const data = input as Record<string, unknown> | null;
|
|
260
|
+
if (!data || typeof data.title !== "string") {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
errors: [
|
|
264
|
+
{ path: "title", expected: "string", actual: typeof data?.title },
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { success: true, data };
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const adapter = fastifyServer({ logger: false });
|
|
273
|
+
adapter.registerRoutes(
|
|
274
|
+
routeTable,
|
|
275
|
+
handlerMap,
|
|
276
|
+
emptyMiddlewareChain(),
|
|
277
|
+
validatorMap,
|
|
278
|
+
);
|
|
279
|
+
const handle = await adapter.listen(0);
|
|
280
|
+
try {
|
|
281
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
282
|
+
const port = native.addresses()[0].port;
|
|
283
|
+
|
|
284
|
+
// Send invalid body (missing title)
|
|
285
|
+
const { status, body } = await fetchJson(port, "/items", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: JSON.stringify({ invalid: true }),
|
|
288
|
+
});
|
|
289
|
+
expect(status).toBe(400);
|
|
290
|
+
expect((body as Record<string, unknown>).error).toBeDefined();
|
|
291
|
+
} finally {
|
|
292
|
+
await handle.close();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("runs response serialization with custom serializer", async () => {
|
|
297
|
+
const routeTable: CompiledRouteTable = {
|
|
298
|
+
segment: "",
|
|
299
|
+
children: {
|
|
300
|
+
data: {
|
|
301
|
+
segment: "data",
|
|
302
|
+
handlers: {
|
|
303
|
+
GET: {
|
|
304
|
+
ref: "data#get",
|
|
305
|
+
middleware: [],
|
|
306
|
+
serializer: "data#get.response",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const serializerCalls: unknown[] = [];
|
|
314
|
+
const handlerMap: HandlerMap = {
|
|
315
|
+
"data#get": async () => ({
|
|
316
|
+
status: 200,
|
|
317
|
+
headers: {},
|
|
318
|
+
body: { value: 42 },
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const adapter = fastifyServer({ logger: false });
|
|
323
|
+
adapter.registerRoutes(
|
|
324
|
+
routeTable,
|
|
325
|
+
handlerMap,
|
|
326
|
+
emptyMiddlewareChain(),
|
|
327
|
+
undefined,
|
|
328
|
+
{
|
|
329
|
+
"data#get.response": (input: unknown) => {
|
|
330
|
+
serializerCalls.push(input);
|
|
331
|
+
return JSON.stringify(input);
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
const handle = await adapter.listen(0);
|
|
336
|
+
try {
|
|
337
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
338
|
+
const port = native.addresses()[0].port;
|
|
339
|
+
|
|
340
|
+
const { status, body } = await fetchJson(port, "/data");
|
|
341
|
+
expect(status).toBe(200);
|
|
342
|
+
expect((body as Record<string, unknown>).value).toBe(42);
|
|
343
|
+
expect(serializerCalls.length).toBe(1);
|
|
344
|
+
} finally {
|
|
345
|
+
await handle.close();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("options are passed to Fastify constructor", () => {
|
|
350
|
+
const adapter = fastifyServer({ logger: false, maxParamLength: 200 });
|
|
351
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
352
|
+
// Verify the instance was created (basic check)
|
|
353
|
+
expect(native).toBeDefined();
|
|
354
|
+
expect(typeof native.listen).toBe("function");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("middleware chain runs before handler", async () => {
|
|
358
|
+
const callOrder: string[] = [];
|
|
359
|
+
|
|
360
|
+
const routeTable: CompiledRouteTable = {
|
|
361
|
+
segment: "",
|
|
362
|
+
children: {
|
|
363
|
+
test: {
|
|
364
|
+
segment: "test",
|
|
365
|
+
handlers: {
|
|
366
|
+
GET: { ref: "test#handler", middleware: ["mw1"] },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const handlerMap: HandlerMap = {
|
|
373
|
+
"test#handler": async (_req: TypoKitRequest, ctx: RequestContext) => {
|
|
374
|
+
callOrder.push("handler");
|
|
375
|
+
return {
|
|
376
|
+
status: 200,
|
|
377
|
+
headers: {},
|
|
378
|
+
body: { requestId: ctx.requestId },
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const middlewareChain: MiddlewareChain = {
|
|
384
|
+
entries: [
|
|
385
|
+
{
|
|
386
|
+
name: "mw1",
|
|
387
|
+
handler: async (_req, _ctx, next) => {
|
|
388
|
+
callOrder.push("middleware");
|
|
389
|
+
return next();
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const adapter = fastifyServer({ logger: false });
|
|
396
|
+
adapter.registerRoutes(routeTable, handlerMap, middlewareChain);
|
|
397
|
+
const handle = await adapter.listen(0);
|
|
398
|
+
try {
|
|
399
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
400
|
+
const port = native.addresses()[0].port;
|
|
401
|
+
|
|
402
|
+
const { status } = await fetchJson(port, "/test");
|
|
403
|
+
expect(status).toBe(200);
|
|
404
|
+
expect(callOrder[0]).toBe("middleware");
|
|
405
|
+
expect(callOrder[1]).toBe("handler");
|
|
406
|
+
} finally {
|
|
407
|
+
await handle.close();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("listen on port 0 assigns auto port", async () => {
|
|
412
|
+
const adapter = fastifyServer({ logger: false });
|
|
413
|
+
adapter.registerRoutes(
|
|
414
|
+
makeRouteTable(),
|
|
415
|
+
makeHandlerMap(),
|
|
416
|
+
emptyMiddlewareChain(),
|
|
417
|
+
);
|
|
418
|
+
const handle = await adapter.listen(0);
|
|
419
|
+
try {
|
|
420
|
+
const native = adapter.getNativeServer!() as FastifyInstance;
|
|
421
|
+
const port = native.addresses()[0].port;
|
|
422
|
+
expect(port).toBeGreaterThan(0);
|
|
423
|
+
} finally {
|
|
424
|
+
await handle.close();
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// @typokit/server-fastify — Fastify Server Adapter
|
|
2
|
+
//
|
|
3
|
+
// Translates TypoKit's compiled route table into Fastify-native route
|
|
4
|
+
// registrations. Fastify-native middleware runs before TypoKit middleware
|
|
5
|
+
// per the architecture (Section 6.3).
|
|
6
|
+
|
|
7
|
+
import Fastify from "fastify";
|
|
8
|
+
import type {
|
|
9
|
+
FastifyInstance,
|
|
10
|
+
FastifyRequest,
|
|
11
|
+
FastifyReply,
|
|
12
|
+
FastifyServerOptions,
|
|
13
|
+
} from "fastify";
|
|
14
|
+
import type {
|
|
15
|
+
CompiledRoute,
|
|
16
|
+
CompiledRouteTable,
|
|
17
|
+
ErrorResponse,
|
|
18
|
+
HandlerMap,
|
|
19
|
+
HttpMethod,
|
|
20
|
+
MiddlewareChain,
|
|
21
|
+
SerializerMap,
|
|
22
|
+
ServerHandle,
|
|
23
|
+
TypoKitRequest,
|
|
24
|
+
TypoKitResponse,
|
|
25
|
+
ValidatorMap,
|
|
26
|
+
ValidationFieldError,
|
|
27
|
+
} from "@typokit/types";
|
|
28
|
+
import type { ServerAdapter, MiddlewareEntry } from "@typokit/core";
|
|
29
|
+
import { createRequestContext, executeMiddlewareChain } from "@typokit/core";
|
|
30
|
+
|
|
31
|
+
// ─── Route Traversal ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
interface RouteEntry {
|
|
34
|
+
method: HttpMethod;
|
|
35
|
+
path: string;
|
|
36
|
+
handlerRef: string;
|
|
37
|
+
validators?: { params?: string; query?: string; body?: string };
|
|
38
|
+
serializer?: string;
|
|
39
|
+
middleware: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Recursively walk the compiled radix tree and collect all registered routes
|
|
44
|
+
* as flat entries with their full paths reconstructed.
|
|
45
|
+
*/
|
|
46
|
+
function collectRoutes(
|
|
47
|
+
node: CompiledRoute,
|
|
48
|
+
prefix: string,
|
|
49
|
+
entries: RouteEntry[],
|
|
50
|
+
): void {
|
|
51
|
+
if (node.handlers) {
|
|
52
|
+
for (const [method, handler] of Object.entries(node.handlers)) {
|
|
53
|
+
if (handler) {
|
|
54
|
+
entries.push({
|
|
55
|
+
method: method as HttpMethod,
|
|
56
|
+
path: prefix || "/",
|
|
57
|
+
handlerRef: handler.ref,
|
|
58
|
+
validators: handler.validators,
|
|
59
|
+
serializer: handler.serializer,
|
|
60
|
+
middleware: handler.middleware,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Static children
|
|
67
|
+
if (node.children) {
|
|
68
|
+
for (const [segment, child] of Object.entries(node.children)) {
|
|
69
|
+
collectRoutes(child, `${prefix}/${segment}`, entries);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Param child (:id)
|
|
74
|
+
if (node.paramChild) {
|
|
75
|
+
const paramNode = node.paramChild;
|
|
76
|
+
collectRoutes(paramNode, `${prefix}/:${paramNode.paramName}`, entries);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wildcard child (*path)
|
|
80
|
+
if (node.wildcardChild) {
|
|
81
|
+
const wildcardNode = node.wildcardChild;
|
|
82
|
+
collectRoutes(wildcardNode, `${prefix}/*`, entries);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Request Validation Pipeline ─────────────────────────────
|
|
87
|
+
|
|
88
|
+
function validationErrorResponse(
|
|
89
|
+
message: string,
|
|
90
|
+
fields: ValidationFieldError[],
|
|
91
|
+
): TypoKitResponse {
|
|
92
|
+
const body: ErrorResponse = {
|
|
93
|
+
error: {
|
|
94
|
+
code: "VALIDATION_ERROR",
|
|
95
|
+
message,
|
|
96
|
+
details: { fields },
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
status: 400,
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runValidators(
|
|
107
|
+
routeHandler: {
|
|
108
|
+
validators?: { params?: string; query?: string; body?: string };
|
|
109
|
+
},
|
|
110
|
+
validatorMap: ValidatorMap | null,
|
|
111
|
+
params: Record<string, string>,
|
|
112
|
+
query: Record<string, string | string[] | undefined>,
|
|
113
|
+
body: unknown,
|
|
114
|
+
): TypoKitResponse | undefined {
|
|
115
|
+
if (!validatorMap || !routeHandler.validators) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const allErrors: ValidationFieldError[] = [];
|
|
120
|
+
|
|
121
|
+
if (routeHandler.validators.params) {
|
|
122
|
+
const validator = validatorMap[routeHandler.validators.params];
|
|
123
|
+
if (validator) {
|
|
124
|
+
const result = validator(params);
|
|
125
|
+
if (!result.success && result.errors) {
|
|
126
|
+
for (const e of result.errors) {
|
|
127
|
+
allErrors.push({
|
|
128
|
+
path: `params.${e.path}`,
|
|
129
|
+
expected: e.expected,
|
|
130
|
+
actual: e.actual,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (routeHandler.validators.query) {
|
|
138
|
+
const validator = validatorMap[routeHandler.validators.query];
|
|
139
|
+
if (validator) {
|
|
140
|
+
const result = validator(query);
|
|
141
|
+
if (!result.success && result.errors) {
|
|
142
|
+
for (const e of result.errors) {
|
|
143
|
+
allErrors.push({
|
|
144
|
+
path: `query.${e.path}`,
|
|
145
|
+
expected: e.expected,
|
|
146
|
+
actual: e.actual,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (routeHandler.validators.body) {
|
|
154
|
+
const validator = validatorMap[routeHandler.validators.body];
|
|
155
|
+
if (validator) {
|
|
156
|
+
const result = validator(body);
|
|
157
|
+
if (!result.success && result.errors) {
|
|
158
|
+
for (const e of result.errors) {
|
|
159
|
+
allErrors.push({
|
|
160
|
+
path: `body.${e.path}`,
|
|
161
|
+
expected: e.expected,
|
|
162
|
+
actual: e.actual,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (allErrors.length > 0) {
|
|
170
|
+
return validationErrorResponse("Request validation failed", allErrors);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Response Serialization Pipeline ──────────────────────────
|
|
177
|
+
|
|
178
|
+
function serializeResponse(
|
|
179
|
+
response: TypoKitResponse,
|
|
180
|
+
serializerRef: string | undefined,
|
|
181
|
+
serializerMap: SerializerMap | null,
|
|
182
|
+
): TypoKitResponse {
|
|
183
|
+
if (
|
|
184
|
+
response.body === null ||
|
|
185
|
+
response.body === undefined ||
|
|
186
|
+
typeof response.body === "string"
|
|
187
|
+
) {
|
|
188
|
+
return response;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const headers = { ...response.headers };
|
|
192
|
+
if (!headers["content-type"]) {
|
|
193
|
+
headers["content-type"] = "application/json";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (serializerRef && serializerMap) {
|
|
197
|
+
const serializer = serializerMap[serializerRef];
|
|
198
|
+
if (serializer) {
|
|
199
|
+
return {
|
|
200
|
+
...response,
|
|
201
|
+
headers,
|
|
202
|
+
body: serializer(response.body),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
...response,
|
|
209
|
+
headers,
|
|
210
|
+
body: JSON.stringify(response.body),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Fastify Server Adapter ──────────────────────────────────
|
|
215
|
+
|
|
216
|
+
interface FastifyServerState {
|
|
217
|
+
routeTable: CompiledRouteTable | null;
|
|
218
|
+
handlerMap: HandlerMap | null;
|
|
219
|
+
middlewareChain: MiddlewareChain | null;
|
|
220
|
+
validatorMap: ValidatorMap | null;
|
|
221
|
+
serializerMap: SerializerMap | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a Fastify server adapter for TypoKit.
|
|
226
|
+
*
|
|
227
|
+
* Options are passed directly to the Fastify constructor (logger, trustProxy, etc.).
|
|
228
|
+
*
|
|
229
|
+
* ```ts
|
|
230
|
+
* import { fastifyServer } from "@typokit/server-fastify";
|
|
231
|
+
* const adapter = fastifyServer({ logger: true, trustProxy: true });
|
|
232
|
+
* adapter.registerRoutes(routeTable, handlerMap, middlewareChain, validatorMap);
|
|
233
|
+
* const handle = await adapter.listen(3000);
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function fastifyServer(options?: FastifyServerOptions): ServerAdapter {
|
|
237
|
+
const app: FastifyInstance = Fastify(options ?? {});
|
|
238
|
+
|
|
239
|
+
const state: FastifyServerState = {
|
|
240
|
+
routeTable: null,
|
|
241
|
+
handlerMap: null,
|
|
242
|
+
middlewareChain: null,
|
|
243
|
+
validatorMap: null,
|
|
244
|
+
serializerMap: null,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/** Convert Fastify request to TypoKitRequest */
|
|
248
|
+
function normalizeRequest(raw: unknown): TypoKitRequest {
|
|
249
|
+
const req = raw as FastifyRequest;
|
|
250
|
+
const headers: Record<string, string | string[] | undefined> = {};
|
|
251
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
252
|
+
headers[key] = value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const rawQuery = req.query as
|
|
256
|
+
| Record<string, string | string[] | undefined>
|
|
257
|
+
| undefined;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
method: req.method.toUpperCase() as HttpMethod,
|
|
261
|
+
path: req.url.split("?")[0],
|
|
262
|
+
headers,
|
|
263
|
+
body: req.body,
|
|
264
|
+
query: rawQuery ?? {},
|
|
265
|
+
params: (req.params as Record<string, string>) ?? {},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Write TypoKitResponse to Fastify reply */
|
|
270
|
+
function writeResponse(raw: unknown, response: TypoKitResponse): void {
|
|
271
|
+
const reply = raw as FastifyReply;
|
|
272
|
+
|
|
273
|
+
// Set headers
|
|
274
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
275
|
+
if (value !== undefined) {
|
|
276
|
+
reply.header(key, value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
reply.status(response.status);
|
|
281
|
+
|
|
282
|
+
if (response.body === null || response.body === undefined) {
|
|
283
|
+
reply.send("");
|
|
284
|
+
} else {
|
|
285
|
+
reply.send(response.body);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const adapter: ServerAdapter = {
|
|
290
|
+
name: "fastify",
|
|
291
|
+
|
|
292
|
+
registerRoutes(
|
|
293
|
+
routeTable: CompiledRouteTable,
|
|
294
|
+
handlerMap: HandlerMap,
|
|
295
|
+
middlewareChain: MiddlewareChain,
|
|
296
|
+
validatorMap?: ValidatorMap,
|
|
297
|
+
serializerMap?: SerializerMap,
|
|
298
|
+
): void {
|
|
299
|
+
state.routeTable = routeTable;
|
|
300
|
+
state.handlerMap = handlerMap;
|
|
301
|
+
state.middlewareChain = middlewareChain;
|
|
302
|
+
state.validatorMap = validatorMap ?? null;
|
|
303
|
+
state.serializerMap = serializerMap ?? null;
|
|
304
|
+
|
|
305
|
+
// Collect all routes from the compiled radix tree
|
|
306
|
+
const routes: RouteEntry[] = [];
|
|
307
|
+
collectRoutes(routeTable, "", routes);
|
|
308
|
+
|
|
309
|
+
// Register each route as a Fastify-native route
|
|
310
|
+
for (const route of routes) {
|
|
311
|
+
app.route({
|
|
312
|
+
method: route.method,
|
|
313
|
+
url: route.path,
|
|
314
|
+
handler: async (req: FastifyRequest, reply: FastifyReply) => {
|
|
315
|
+
const typoReq = normalizeRequest(req);
|
|
316
|
+
|
|
317
|
+
// Run request validation pipeline
|
|
318
|
+
const validationError = runValidators(
|
|
319
|
+
{ validators: route.validators },
|
|
320
|
+
state.validatorMap,
|
|
321
|
+
typoReq.params,
|
|
322
|
+
typoReq.query,
|
|
323
|
+
typoReq.body,
|
|
324
|
+
);
|
|
325
|
+
if (validationError) {
|
|
326
|
+
writeResponse(reply, validationError);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const handlerFn = state.handlerMap![route.handlerRef];
|
|
331
|
+
if (!handlerFn) {
|
|
332
|
+
const errorResp: TypoKitResponse = {
|
|
333
|
+
status: 500,
|
|
334
|
+
headers: { "content-type": "application/json" },
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
error: "Internal Server Error",
|
|
337
|
+
message: `Handler not found: ${route.handlerRef}`,
|
|
338
|
+
}),
|
|
339
|
+
};
|
|
340
|
+
writeResponse(reply, errorResp);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Create request context and execute middleware chain
|
|
345
|
+
let ctx = createRequestContext();
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
state.middlewareChain &&
|
|
349
|
+
state.middlewareChain.entries.length > 0
|
|
350
|
+
) {
|
|
351
|
+
const entries: MiddlewareEntry[] =
|
|
352
|
+
state.middlewareChain.entries.map((e) => ({
|
|
353
|
+
name: e.name,
|
|
354
|
+
middleware: {
|
|
355
|
+
handler: async (input) => {
|
|
356
|
+
const mwReq: TypoKitRequest = {
|
|
357
|
+
method: typoReq.method,
|
|
358
|
+
path: typoReq.path,
|
|
359
|
+
headers: input.headers,
|
|
360
|
+
body: input.body,
|
|
361
|
+
query: input.query,
|
|
362
|
+
params: input.params,
|
|
363
|
+
};
|
|
364
|
+
const response = await e.handler(
|
|
365
|
+
mwReq,
|
|
366
|
+
input.ctx,
|
|
367
|
+
async () => {
|
|
368
|
+
return { status: 200, headers: {}, body: null };
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
return response as unknown as Record<string, unknown>;
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
ctx = await executeMiddlewareChain(typoReq, ctx, entries);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Call the handler
|
|
380
|
+
const response = await handlerFn(typoReq, ctx);
|
|
381
|
+
|
|
382
|
+
// Response serialization pipeline
|
|
383
|
+
const serialized = serializeResponse(
|
|
384
|
+
response,
|
|
385
|
+
route.serializer,
|
|
386
|
+
state.serializerMap,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
writeResponse(reply, serialized);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
async listen(port: number): Promise<ServerHandle> {
|
|
396
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
async close(): Promise<void> {
|
|
400
|
+
await app.close();
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
normalizeRequest,
|
|
406
|
+
writeResponse,
|
|
407
|
+
|
|
408
|
+
getNativeServer(): unknown {
|
|
409
|
+
return app;
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return adapter;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Re-export for convenience
|
|
417
|
+
export { serializeResponse, runValidators, validationErrorResponse };
|
|
418
|
+
export { type ServerAdapter } from "@typokit/core";
|