@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.
@@ -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";