@codewithagents/openapi-server 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -2
- package/dist/cli.cjs +292 -44
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +6 -2
- package/dist/generator.js.map +1 -1
- package/dist/plugins/router.d.ts +13 -0
- package/dist/plugins/router.d.ts.map +1 -1
- package/dist/plugins/router.js +405 -43
- package/dist/plugins/router.js.map +1 -1
- package/dist/plugins/service.d.ts +17 -1
- package/dist/plugins/service.d.ts.map +1 -1
- package/dist/plugins/service.js +23 -4
- package/dist/plugins/service.js.map +1 -1
- package/dist/plugins/shared.d.ts +1 -1
- package/dist/plugins/shared.d.ts.map +1 -1
- package/dist/plugins/shared.js +13 -0
- package/dist/plugins/shared.js.map +1 -1
- package/package.json +1 -1
package/dist/plugins/router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SUPPORTED_METHODS, isRef, extractPathParamsFromPath, resolveParam, deriveServiceName, deriveMethodName, getQueryParams, getBodyInfo, } from './shared.js';
|
|
1
|
+
import { SUPPORTED_METHODS, isRef, refToName, extractPathParamsFromPath, resolveParam, deriveServiceName, deriveMethodName, getQueryParams, getBodyInfo, } from './shared.js';
|
|
2
2
|
/** Convert OpenAPI path to Hono path: {id} -> :id */
|
|
3
3
|
function toHonoPath(openapiPath) {
|
|
4
4
|
return openapiPath.replace(/\{([^}]+)\}/g, ':$1');
|
|
@@ -83,9 +83,11 @@ function queryParamDeepObjectZodBase(param) {
|
|
|
83
83
|
});
|
|
84
84
|
return `z.object({ ${propFields.join(', ')} })`;
|
|
85
85
|
}
|
|
86
|
-
/** Number/integer param: z.number() with optional range modifiers.
|
|
86
|
+
/** Number/integer param: z.coerce.number() with optional range modifiers.
|
|
87
|
+
* Uses coerce so that Fastify's raw string values (fast-querystring never converts types)
|
|
88
|
+
* are accepted alongside the already-coerced numbers from Express/Hono extraction. */
|
|
87
89
|
function queryParamNumberZodBase(param) {
|
|
88
|
-
let base = 'z.number()';
|
|
90
|
+
let base = 'z.coerce.number()';
|
|
89
91
|
if (param.minimum !== undefined)
|
|
90
92
|
base += `.min(${param.minimum})`;
|
|
91
93
|
if (param.maximum !== undefined)
|
|
@@ -191,6 +193,28 @@ function getPathParamValidations(operation, spec, rawPathParamNames) {
|
|
|
191
193
|
}
|
|
192
194
|
return result;
|
|
193
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Build a Zod expression for a cookie parameter based on its captured constraints.
|
|
198
|
+
* Cookie values are always strings; emits z.string() or z.enum([...]) with optional
|
|
199
|
+
* pattern/length modifiers. Appends .optional() for non-required params.
|
|
200
|
+
*/
|
|
201
|
+
function cookieParamZodExpr(param) {
|
|
202
|
+
let base;
|
|
203
|
+
if (param.enum !== undefined && param.enum.length > 0) {
|
|
204
|
+
const members = param.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
205
|
+
base = `z.enum([${members}])`;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
base = 'z.string()';
|
|
209
|
+
}
|
|
210
|
+
if (param.minLength !== undefined)
|
|
211
|
+
base += `.min(${param.minLength})`;
|
|
212
|
+
if (param.maxLength !== undefined)
|
|
213
|
+
base += `.max(${param.maxLength})`;
|
|
214
|
+
if (param.pattern !== undefined)
|
|
215
|
+
base += `.regex(/${param.pattern}/)`;
|
|
216
|
+
return param.required ? base : `${base}.optional()`;
|
|
217
|
+
}
|
|
194
218
|
/**
|
|
195
219
|
* Collect header parameters from an operation, including schema constraints.
|
|
196
220
|
*/
|
|
@@ -223,6 +247,39 @@ function getHeaderParams(operation, spec) {
|
|
|
223
247
|
}
|
|
224
248
|
return result;
|
|
225
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Collect cookie parameters (in: cookie) from an operation, including schema constraints.
|
|
252
|
+
* Cookie names are case-sensitive and are used as-is for both the Zod field key and value lookup.
|
|
253
|
+
*/
|
|
254
|
+
function getCookieParams(operation, spec) {
|
|
255
|
+
const parameters = operation.parameters;
|
|
256
|
+
if (parameters === undefined)
|
|
257
|
+
return [];
|
|
258
|
+
const result = [];
|
|
259
|
+
for (const p of parameters) {
|
|
260
|
+
const resolved = resolveParam(p, spec);
|
|
261
|
+
if (resolved === undefined || resolved.in !== 'cookie')
|
|
262
|
+
continue;
|
|
263
|
+
const param = {
|
|
264
|
+
rawName: resolved.name,
|
|
265
|
+
required: resolved.required === true,
|
|
266
|
+
};
|
|
267
|
+
const schema = resolved.schema;
|
|
268
|
+
if (schema !== undefined && !isRef(schema)) {
|
|
269
|
+
const s = schema;
|
|
270
|
+
if (Array.isArray(s.enum))
|
|
271
|
+
param.enum = s.enum;
|
|
272
|
+
if (typeof s.minLength === 'number')
|
|
273
|
+
param.minLength = s.minLength;
|
|
274
|
+
if (typeof s.maxLength === 'number')
|
|
275
|
+
param.maxLength = s.maxLength;
|
|
276
|
+
if (typeof s.pattern === 'string')
|
|
277
|
+
param.pattern = s.pattern;
|
|
278
|
+
}
|
|
279
|
+
result.push(param);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
226
283
|
/**
|
|
227
284
|
* Returns true when a query param carries schema constraints beyond basic type/required.
|
|
228
285
|
* These constraints require a Zod validation block even if the param is optional or string.
|
|
@@ -301,7 +358,9 @@ function emitPathValidation(lines, validations, indent, framework) {
|
|
|
301
358
|
access = `c.req.param(${JSON.stringify(v.rawName)})`;
|
|
302
359
|
}
|
|
303
360
|
else if (framework === 'express') {
|
|
304
|
-
|
|
361
|
+
// Cast to string: Express 5 types req.params values as string | string[],
|
|
362
|
+
// but path params are always single strings in practice.
|
|
363
|
+
access = `req.params[${JSON.stringify(v.rawName)}] as string`;
|
|
305
364
|
}
|
|
306
365
|
else {
|
|
307
366
|
access = /[^a-zA-Z0-9_$]/.test(v.rawName)
|
|
@@ -337,15 +396,16 @@ function emitHeaderValidation(lines, headerParams, indent, framework) {
|
|
|
337
396
|
const rawFields = headerParams
|
|
338
397
|
.map((h) => {
|
|
339
398
|
const key = JSON.stringify(h.rawName);
|
|
399
|
+
const lookupKey = JSON.stringify(h.rawName.toLowerCase());
|
|
340
400
|
let access;
|
|
341
401
|
if (framework === 'hono') {
|
|
342
402
|
access = `c.req.header(${key})`;
|
|
343
403
|
}
|
|
344
404
|
else if (framework === 'express') {
|
|
345
|
-
access = `req.headers[${
|
|
405
|
+
access = `req.headers[${lookupKey}] as string | undefined`;
|
|
346
406
|
}
|
|
347
407
|
else {
|
|
348
|
-
access = `req.headers[${
|
|
408
|
+
access = `req.headers[${lookupKey}]`;
|
|
349
409
|
}
|
|
350
410
|
return `${fieldIndent}${key}: ${access}`;
|
|
351
411
|
})
|
|
@@ -357,6 +417,56 @@ function emitHeaderValidation(lines, headerParams, indent, framework) {
|
|
|
357
417
|
lines.push(rawFields);
|
|
358
418
|
lines.push(`${inner}})`);
|
|
359
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Emit Zod validation lines for cookie parameters into the handler line buffer.
|
|
422
|
+
* Cookie names are case-sensitive: the exact name is used for both the Zod field key and
|
|
423
|
+
* the value lookup (unlike headers, which are lowercased for lookup on Express/Fastify).
|
|
424
|
+
*
|
|
425
|
+
* Required plugins per framework:
|
|
426
|
+
* Fastify: register @fastify/cookie before creating the router.
|
|
427
|
+
* Express: apply cookie-parser middleware before mounting this router.
|
|
428
|
+
* Hono: getCookie is imported from 'hono/cookie' (added to generated output automatically).
|
|
429
|
+
*
|
|
430
|
+
* Uses short variable name _ckv to keep the 422 return line under Prettier's print width.
|
|
431
|
+
* @param indent - outer handler indent (e.g. ' ')
|
|
432
|
+
* @param framework - used to generate the correct cookie accessor syntax
|
|
433
|
+
*/
|
|
434
|
+
function emitCookieValidation(lines, cookieParams, indent, framework) {
|
|
435
|
+
const inner = `${indent} `;
|
|
436
|
+
const fieldIndent = `${indent} `;
|
|
437
|
+
const schemaFields = cookieParams
|
|
438
|
+
.map((ck) => {
|
|
439
|
+
const key = JSON.stringify(ck.rawName);
|
|
440
|
+
const expr = cookieParamZodExpr(ck);
|
|
441
|
+
return `${fieldIndent}${key}: ${expr}`;
|
|
442
|
+
})
|
|
443
|
+
.join(',\n');
|
|
444
|
+
const rawFields = cookieParams
|
|
445
|
+
.map((ck) => {
|
|
446
|
+
const key = JSON.stringify(ck.rawName);
|
|
447
|
+
let access;
|
|
448
|
+
if (framework === 'hono') {
|
|
449
|
+
// getCookie is imported from 'hono/cookie' when cookie params are present.
|
|
450
|
+
access = `getCookie(c, ${key})`;
|
|
451
|
+
}
|
|
452
|
+
else if (framework === 'express') {
|
|
453
|
+
// Requires cookie-parser middleware: req.cookies['name']
|
|
454
|
+
access = `req.cookies[${key}] as string | undefined`;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
// Requires @fastify/cookie plugin: req.cookies['name']
|
|
458
|
+
access = `req.cookies[${key}]`;
|
|
459
|
+
}
|
|
460
|
+
return `${fieldIndent}${key}: ${access}`;
|
|
461
|
+
})
|
|
462
|
+
.join(',\n');
|
|
463
|
+
lines.push(`${inner}// Validate request cookies: returns 422 with Zod issues on failure`);
|
|
464
|
+
lines.push(`${inner}const _ckv = z.object({`);
|
|
465
|
+
lines.push(schemaFields);
|
|
466
|
+
lines.push(`${inner}}).safeParse({`);
|
|
467
|
+
lines.push(rawFields);
|
|
468
|
+
lines.push(`${inner}})`);
|
|
469
|
+
}
|
|
360
470
|
function response200IsVoid(resp) {
|
|
361
471
|
if (isRef(resp))
|
|
362
472
|
return false;
|
|
@@ -443,6 +553,44 @@ function getResponseStatus(operation, httpMethod) {
|
|
|
443
553
|
? { status: 204, isVoid: true, responseContentType: 'application/json' }
|
|
444
554
|
: { status: 200, isVoid: false, responseContentType: 'application/json' };
|
|
445
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Resolve the response type name and shape for Fastify schema.response wiring.
|
|
558
|
+
* Returns the PascalCase type name of the first JSON 2xx response if it is a
|
|
559
|
+
* direct $ref or an array-of-$ref. Returns undefined for inline schemas, void,
|
|
560
|
+
* text/plain, or octet-stream responses.
|
|
561
|
+
*
|
|
562
|
+
* Priority order mirrors service.ts getReturnInfo: 200, 201, then other 2xx codes.
|
|
563
|
+
*/
|
|
564
|
+
function getResponseTypeName(operation) {
|
|
565
|
+
const responses = operation.responses;
|
|
566
|
+
if (responses === undefined)
|
|
567
|
+
return undefined;
|
|
568
|
+
const priority = ['200', '201', ...Object.keys(responses).filter((k) => /^2\d\d$/.test(k) && k !== '200' && k !== '201' && k !== '204')];
|
|
569
|
+
for (const code of priority) {
|
|
570
|
+
const response = responses[code];
|
|
571
|
+
if (response === undefined || isRef(response))
|
|
572
|
+
continue;
|
|
573
|
+
const resp = response;
|
|
574
|
+
const content = resp.content;
|
|
575
|
+
if (content === undefined)
|
|
576
|
+
continue;
|
|
577
|
+
const jsonContent = content['application/json'];
|
|
578
|
+
if (jsonContent === undefined || jsonContent.schema === undefined)
|
|
579
|
+
continue;
|
|
580
|
+
const schema = jsonContent.schema;
|
|
581
|
+
if (isRef(schema)) {
|
|
582
|
+
return { typeName: refToName(schema.$ref), isArray: false };
|
|
583
|
+
}
|
|
584
|
+
const s = schema;
|
|
585
|
+
if (s.type === 'array' && s.items !== undefined && isRef(s.items)) {
|
|
586
|
+
return {
|
|
587
|
+
typeName: refToName(s.items.$ref),
|
|
588
|
+
isArray: true,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
446
594
|
function collectOperations(spec) {
|
|
447
595
|
const paths = spec.paths;
|
|
448
596
|
if (paths === undefined)
|
|
@@ -458,8 +606,10 @@ function collectOperations(spec) {
|
|
|
458
606
|
const pathParamValidations = getPathParamValidations(operation, spec, pathParams);
|
|
459
607
|
const queryParams = getQueryParams(operation, spec);
|
|
460
608
|
const headerParams = getHeaderParams(operation, spec);
|
|
609
|
+
const cookieParams = getCookieParams(operation, spec);
|
|
461
610
|
const bodyInfo = getBodyInfo(operation);
|
|
462
611
|
const responseStatus = getResponseStatus(operation, method);
|
|
612
|
+
const responseTypeInfo = getResponseTypeName(operation);
|
|
463
613
|
operations.push({
|
|
464
614
|
methodName,
|
|
465
615
|
httpMethod: method,
|
|
@@ -469,8 +619,11 @@ function collectOperations(spec) {
|
|
|
469
619
|
pathParamValidations,
|
|
470
620
|
queryParams,
|
|
471
621
|
headerParams,
|
|
622
|
+
cookieParams,
|
|
472
623
|
bodyInfo,
|
|
473
624
|
responseStatus,
|
|
625
|
+
responseTypeName: responseTypeInfo?.typeName,
|
|
626
|
+
responseIsArray: responseTypeInfo?.isArray,
|
|
474
627
|
});
|
|
475
628
|
}
|
|
476
629
|
}
|
|
@@ -502,6 +655,26 @@ function collectUsedSchemaNames(operations, schemaNames) {
|
|
|
502
655
|
}
|
|
503
656
|
return used;
|
|
504
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Collect the subset of schemaNames used as Fastify response schemas.
|
|
660
|
+
* Only operations with a resolvable $ref response type (direct or array-of-$ref)
|
|
661
|
+
* and a matching schema in schemaNames are included.
|
|
662
|
+
* Multi-status operations are excluded because they cannot be mapped to a single
|
|
663
|
+
* status code in schema.response at generation time.
|
|
664
|
+
*/
|
|
665
|
+
function collectUsedResponseSchemaNames(operations, schemaNames) {
|
|
666
|
+
const used = new Set();
|
|
667
|
+
for (const op of operations) {
|
|
668
|
+
if (op.responseTypeName === undefined)
|
|
669
|
+
continue;
|
|
670
|
+
if (op.responseStatus.isMultiStatus === true)
|
|
671
|
+
continue;
|
|
672
|
+
const schemaName = `${op.responseTypeName}Schema`;
|
|
673
|
+
if (schemaNames.has(schemaName))
|
|
674
|
+
used.add(schemaName);
|
|
675
|
+
}
|
|
676
|
+
return used;
|
|
677
|
+
}
|
|
505
678
|
/**
|
|
506
679
|
* Collect body type names, used schema names, and whether Zod is needed.
|
|
507
680
|
* Shared by all three generator functions to avoid duplication.
|
|
@@ -511,13 +684,23 @@ function collectGeneratorSetup(operations, options) {
|
|
|
511
684
|
const usedSchemaNames = options?.schemaNames !== undefined
|
|
512
685
|
? collectUsedSchemaNames(operations, options.schemaNames)
|
|
513
686
|
: new Set();
|
|
687
|
+
const usedResponseSchemaNames = options?.schemaNames !== undefined
|
|
688
|
+
? collectUsedResponseSchemaNames(operations, options.schemaNames)
|
|
689
|
+
: new Set();
|
|
690
|
+
// Zod is needed for param validation, body schema validation, or array response schemas.
|
|
691
|
+
// Array response schemas emit z.array(XSchema) which requires the z import.
|
|
692
|
+
const hasArrayResponseSchema = usedResponseSchemaNames.size > 0 &&
|
|
693
|
+
operations.some((op) => op.responseIsArray === true &&
|
|
694
|
+
op.responseTypeName !== undefined &&
|
|
695
|
+
usedResponseSchemaNames.has(`${op.responseTypeName}Schema`));
|
|
514
696
|
const needsZod = (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) ||
|
|
515
|
-
operationsNeedZodForParams(operations)
|
|
516
|
-
|
|
697
|
+
operationsNeedZodForParams(operations) ||
|
|
698
|
+
(usedResponseSchemaNames.size > 0 && options?.schemaImportPath !== undefined && hasArrayResponseSchema);
|
|
699
|
+
return { sortedBodyTypes, usedSchemaNames, usedResponseSchemaNames, needsZod };
|
|
517
700
|
}
|
|
518
701
|
// ── Hono route handler ────────────────────────────────────────────────────────
|
|
519
702
|
// fallow-ignore-next-line complexity
|
|
520
|
-
function buildRouteHandler(op, indent, schemaNames) {
|
|
703
|
+
function buildRouteHandler(op, indent, schemaNames, contextType) {
|
|
521
704
|
const lines = [];
|
|
522
705
|
lines.push(`${indent}app.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (c) => {`);
|
|
523
706
|
// Path param format validation (e.g. uuid)
|
|
@@ -577,6 +760,14 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
577
760
|
lines.push(`${indent} return c.json({ error: 'Invalid request headers', issues: _hv.error.issues }, 422)`);
|
|
578
761
|
lines.push(`${indent} }`);
|
|
579
762
|
}
|
|
763
|
+
// Cookie param validation
|
|
764
|
+
// Requires @fastify/cookie on Fastify, cookie-parser on Express, getCookie from hono/cookie on Hono.
|
|
765
|
+
if (op.cookieParams.length > 0) {
|
|
766
|
+
emitCookieValidation(lines, op.cookieParams, indent, 'hono');
|
|
767
|
+
lines.push(`${indent} if (!_ckv.success) {`);
|
|
768
|
+
lines.push(`${indent} return c.json({ error: 'Invalid request cookies', issues: _ckv.error.issues }, 422)`);
|
|
769
|
+
lines.push(`${indent} }`);
|
|
770
|
+
}
|
|
580
771
|
// Body extraction
|
|
581
772
|
let bodyVarName = 'body';
|
|
582
773
|
if (op.bodyInfo !== undefined) {
|
|
@@ -625,7 +816,12 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
625
816
|
lines.push(`${indent} if (!parseResult.success) {`);
|
|
626
817
|
lines.push(`${indent} return c.json({ error: 'Invalid request body', issues: parseResult.error.issues }, 422)`);
|
|
627
818
|
lines.push(`${indent} }`);
|
|
628
|
-
|
|
819
|
+
// Forward the validated/coerced data (parseResult.data), NOT the raw parsed body,
|
|
820
|
+
// so Zod coercion is preserved (e.g. form-urlencoded numeric fields via
|
|
821
|
+
// z.coerce.number()). Cast to the declared model type so the service-call type
|
|
822
|
+
// stays correct even when the schema infers a narrower shape (e.g. z.unknown()
|
|
823
|
+
// for inline-union properties); safeParse above guarantees runtime safety.
|
|
824
|
+
lines.push(`${indent} const validatedBody = parseResult.data as ${typeDecl}`);
|
|
629
825
|
bodyVarName = 'validatedBody';
|
|
630
826
|
}
|
|
631
827
|
}
|
|
@@ -638,7 +834,15 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
638
834
|
serviceArgs.push(bodyVarName);
|
|
639
835
|
}
|
|
640
836
|
if (op.queryParams.length > 0) {
|
|
641
|
-
|
|
837
|
+
// After successful Zod validation _qv.data carries the correct required/typed
|
|
838
|
+
// values (e.g. string[] for delimited arrays, object shape for deepObject params,
|
|
839
|
+
// and non-optional values for required scalar params). Use _qv.data when validation
|
|
840
|
+
// was applied; fall back to params when no validation is needed.
|
|
841
|
+
serviceArgs.push(queryParamsNeedValidation(op.queryParams) ? '_qv.data' : 'params');
|
|
842
|
+
}
|
|
843
|
+
// Context arg: pass the Hono Context object (c) as the final argument when contextType is set.
|
|
844
|
+
if (contextType !== undefined) {
|
|
845
|
+
serviceArgs.push('c');
|
|
642
846
|
}
|
|
643
847
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
644
848
|
// Response — wrap in try/catch to map HttpError to its status
|
|
@@ -687,7 +891,7 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
687
891
|
}
|
|
688
892
|
// ── Express route handler ─────────────────────────────────────────────────────
|
|
689
893
|
// fallow-ignore-next-line complexity
|
|
690
|
-
function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
894
|
+
function buildExpressRouteHandler(op, indent, schemaNames, contextType) {
|
|
691
895
|
const lines = [];
|
|
692
896
|
lines.push(`${indent}router.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (req: Request, res: Response) => {`);
|
|
693
897
|
// Path param format validation (e.g. uuid)
|
|
@@ -739,6 +943,14 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
739
943
|
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request headers', issues: _hv.error.issues })`);
|
|
740
944
|
lines.push(`${indent} }`);
|
|
741
945
|
}
|
|
946
|
+
// Cookie param validation
|
|
947
|
+
// Requires cookie-parser middleware: app.use(cookieParser()) before mounting this router.
|
|
948
|
+
if (op.cookieParams.length > 0) {
|
|
949
|
+
emitCookieValidation(lines, op.cookieParams, indent, 'express');
|
|
950
|
+
lines.push(`${indent} if (!_ckv.success) {`);
|
|
951
|
+
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request cookies', issues: _ckv.error.issues })`);
|
|
952
|
+
lines.push(`${indent} }`);
|
|
953
|
+
}
|
|
742
954
|
// Body extraction, with optional Zod validation.
|
|
743
955
|
// For both JSON and form-urlencoded bodies Express pre-populates req.body via middleware
|
|
744
956
|
// (express.json() for JSON, express.urlencoded() for form). The router just reads req.body.
|
|
@@ -761,7 +973,14 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
761
973
|
lines.push(`${indent} if (!parseResult.success) {`);
|
|
762
974
|
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
763
975
|
lines.push(`${indent} }`);
|
|
764
|
-
|
|
976
|
+
// Cast parseResult.data to the declared model type so the service call receives
|
|
977
|
+
// the correct TypeScript type even when the Zod schema infers a narrower or
|
|
978
|
+
// different shape (e.g. z.unknown() for inline-union properties). The cast is
|
|
979
|
+
// safe because safeParse already confirmed the value is structurally valid.
|
|
980
|
+
const typeDecl = op.bodyInfo.typeName !== undefined && !op.bodyInfo.isSynthesized
|
|
981
|
+
? op.bodyInfo.typeName
|
|
982
|
+
: 'unknown';
|
|
983
|
+
lines.push(`${indent} const validatedBody = parseResult.data as ${typeDecl}`);
|
|
765
984
|
bodyVarName = 'validatedBody';
|
|
766
985
|
}
|
|
767
986
|
else {
|
|
@@ -776,13 +995,23 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
776
995
|
// Build service call args
|
|
777
996
|
const serviceArgs = [];
|
|
778
997
|
for (const p of op.pathParams) {
|
|
779
|
-
|
|
998
|
+
// Cast to string: Express 5 types req.params values as string | string[],
|
|
999
|
+
// but path params are always single strings in practice.
|
|
1000
|
+
serviceArgs.push(`(req.params['${p}'] as string)`);
|
|
780
1001
|
}
|
|
781
1002
|
if (op.bodyInfo !== undefined) {
|
|
782
1003
|
serviceArgs.push(bodyVarName);
|
|
783
1004
|
}
|
|
784
1005
|
if (op.queryParams.length > 0) {
|
|
785
|
-
|
|
1006
|
+
// After successful Zod validation _qv.data carries the correct required/typed
|
|
1007
|
+
// values (e.g. string[] for delimited arrays, object shape for deepObject params,
|
|
1008
|
+
// and non-optional values for required scalar params). Use _qv.data when validation
|
|
1009
|
+
// was applied; fall back to params when no validation is needed.
|
|
1010
|
+
serviceArgs.push(queryParamsNeedValidation(op.queryParams) ? '_qv.data' : 'params');
|
|
1011
|
+
}
|
|
1012
|
+
// Context arg: pass the Express Request object (req) as the final argument when contextType is set.
|
|
1013
|
+
if (contextType !== undefined) {
|
|
1014
|
+
serviceArgs.push('req');
|
|
786
1015
|
}
|
|
787
1016
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
788
1017
|
// Response — wrap in try/catch to map HttpError to its status
|
|
@@ -828,8 +1057,45 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
828
1057
|
return lines.join('\n');
|
|
829
1058
|
}
|
|
830
1059
|
// ── Fastify route handler ─────────────────────────────────────────────────────
|
|
1060
|
+
/**
|
|
1061
|
+
* Build the route options object literal string for a Fastify route registration.
|
|
1062
|
+
* Always includes config.operationId so onRequest hooks can identify the operation
|
|
1063
|
+
* via request.routeOptions.config.operationId (issue #309).
|
|
1064
|
+
* When a response schema is available in schemaNames, also includes schema.response
|
|
1065
|
+
* so Fastify validates the response against the Zod schema (issue #308).
|
|
1066
|
+
* Both properties are merged into a single options object literal when both apply.
|
|
1067
|
+
*/
|
|
1068
|
+
function buildFastifyRouteOptions(op, schemaNames) {
|
|
1069
|
+
const parts = [];
|
|
1070
|
+
const responseSchemaExpr = buildFastifyResponseSchemaExpr(op, schemaNames);
|
|
1071
|
+
if (responseSchemaExpr !== undefined) {
|
|
1072
|
+
parts.push(`schema: { response: { ${op.responseStatus.status}: ${responseSchemaExpr} } }`);
|
|
1073
|
+
}
|
|
1074
|
+
parts.push(`config: { operationId: '${op.methodName}' }`);
|
|
1075
|
+
return `{ ${parts.join(', ')} }`;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Build the Zod schema expression for a Fastify schema.response entry.
|
|
1079
|
+
* Returns undefined when no schema is available or the operation is multi-status.
|
|
1080
|
+
* Direct $ref response: 'PetSchema'
|
|
1081
|
+
* Array-of-$ref response: 'z.array(PetSchema)'
|
|
1082
|
+
*/
|
|
1083
|
+
function buildFastifyResponseSchemaExpr(op, schemaNames) {
|
|
1084
|
+
if (schemaNames === undefined)
|
|
1085
|
+
return undefined;
|
|
1086
|
+
if (op.responseTypeName === undefined)
|
|
1087
|
+
return undefined;
|
|
1088
|
+
if (op.responseStatus.isMultiStatus === true)
|
|
1089
|
+
return undefined;
|
|
1090
|
+
if (op.responseStatus.isVoid)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const schemaName = `${op.responseTypeName}Schema`;
|
|
1093
|
+
if (!schemaNames.has(schemaName))
|
|
1094
|
+
return undefined;
|
|
1095
|
+
return op.responseIsArray === true ? `z.array(${schemaName})` : schemaName;
|
|
1096
|
+
}
|
|
831
1097
|
// fallow-ignore-next-line complexity
|
|
832
|
-
function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
1098
|
+
function buildFastifyRouteHandler(op, indent, schemaNames, contextType) {
|
|
833
1099
|
const lines = [];
|
|
834
1100
|
// Build generic type argument
|
|
835
1101
|
const genericParts = [];
|
|
@@ -868,12 +1134,13 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
868
1134
|
genericParts.push(`Params: { ${paramFields} }`);
|
|
869
1135
|
}
|
|
870
1136
|
const generic = genericParts.length > 0 ? `<{ ${genericParts.join('; ')} }>` : '';
|
|
871
|
-
|
|
1137
|
+
const routeOpts = buildFastifyRouteOptions(op, schemaNames);
|
|
1138
|
+
lines.push(`${indent}app.${op.httpMethod}${generic}(${JSON.stringify(op.honoPath)}, ${routeOpts}, async (req, reply) => {`);
|
|
872
1139
|
// Path param format validation (e.g. uuid)
|
|
873
1140
|
if (op.pathParamValidations.length > 0) {
|
|
874
1141
|
emitPathValidation(lines, op.pathParamValidations, indent, 'fastify');
|
|
875
1142
|
lines.push(`${indent} if (!_pv.success) {`);
|
|
876
|
-
lines.push(`${indent} return reply.status(422).send({`);
|
|
1143
|
+
lines.push(`${indent} return (reply as FastifyReply).status(422).send({`);
|
|
877
1144
|
lines.push(`${indent} error: 'Invalid path parameters',`);
|
|
878
1145
|
lines.push(`${indent} issues: _pv.error.issues,`);
|
|
879
1146
|
lines.push(`${indent} })`);
|
|
@@ -909,6 +1176,13 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
909
1176
|
return ` ${q.name}: typeof _dq['${q.rawName}'] === 'string' ? _dq['${q.rawName}']!.split(${delim}) : undefined`;
|
|
910
1177
|
}
|
|
911
1178
|
// When _dq is defined, use it for consistent access; otherwise use typed req.query.
|
|
1179
|
+
// Boolean params must be coerced from string at extraction time: z.coerce.boolean()
|
|
1180
|
+
// is unusable because Boolean('false') === true, so mirror the Express === 'true' pattern.
|
|
1181
|
+
if (q.tsType === 'boolean') {
|
|
1182
|
+
return hasDeepOrDelimited
|
|
1183
|
+
? ` ${q.name}: _dq['${q.rawName}'] === 'true'`
|
|
1184
|
+
: ` ${q.name}: (req.query.${q.name} as unknown as string) === 'true'`;
|
|
1185
|
+
}
|
|
912
1186
|
return hasDeepOrDelimited
|
|
913
1187
|
? ` ${q.name}: _dq['${q.rawName}']`
|
|
914
1188
|
: ` ${q.name}: req.query.${q.name}`;
|
|
@@ -921,7 +1195,7 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
921
1195
|
if (queryParamsNeedValidation(op.queryParams)) {
|
|
922
1196
|
emitQueryValidation(lines, op.queryParams, indent);
|
|
923
1197
|
lines.push(`${indent} if (!_qv.success) {`);
|
|
924
|
-
lines.push(`${indent} return reply.status(422).send({`);
|
|
1198
|
+
lines.push(`${indent} return (reply as FastifyReply).status(422).send({`);
|
|
925
1199
|
lines.push(`${indent} error: 'Invalid query parameters',`);
|
|
926
1200
|
lines.push(`${indent} issues: _qv.error.issues,`);
|
|
927
1201
|
lines.push(`${indent} })`);
|
|
@@ -932,22 +1206,43 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
932
1206
|
if (op.headerParams.length > 0) {
|
|
933
1207
|
emitHeaderValidation(lines, op.headerParams, indent, 'fastify');
|
|
934
1208
|
lines.push(`${indent} if (!_hv.success) {`);
|
|
935
|
-
lines.push(`${indent} return reply.status(422).send({`);
|
|
1209
|
+
lines.push(`${indent} return (reply as FastifyReply).status(422).send({`);
|
|
936
1210
|
lines.push(`${indent} error: 'Invalid request headers',`);
|
|
937
1211
|
lines.push(`${indent} issues: _hv.error.issues,`);
|
|
938
1212
|
lines.push(`${indent} })`);
|
|
939
1213
|
lines.push(`${indent} }`);
|
|
940
1214
|
}
|
|
1215
|
+
// Cookie param validation
|
|
1216
|
+
// Requires @fastify/cookie plugin registered before creating the router: fastify.register(fastifyCookie).
|
|
1217
|
+
if (op.cookieParams.length > 0) {
|
|
1218
|
+
emitCookieValidation(lines, op.cookieParams, indent, 'fastify');
|
|
1219
|
+
lines.push(`${indent} if (!_ckv.success) {`);
|
|
1220
|
+
lines.push(`${indent} return (reply as FastifyReply).status(422).send({`);
|
|
1221
|
+
lines.push(`${indent} error: 'Invalid request cookies',`);
|
|
1222
|
+
lines.push(`${indent} issues: _ckv.error.issues,`);
|
|
1223
|
+
lines.push(`${indent} })`);
|
|
1224
|
+
lines.push(`${indent} }`);
|
|
1225
|
+
}
|
|
941
1226
|
// Body handling, with optional Zod validation.
|
|
942
|
-
// Fastify
|
|
943
|
-
// For
|
|
944
|
-
// so that req.body
|
|
1227
|
+
// Fastify natively parses only application/json and text/plain.
|
|
1228
|
+
// For application/x-www-form-urlencoded: register @fastify/formbody before this router
|
|
1229
|
+
// so that req.body is populated.
|
|
1230
|
+
// For multipart/form-data: register @fastify/multipart with attachFieldsToBody: true before
|
|
1231
|
+
// this router so that req.body is populated with parsed fields. Without attachFieldsToBody,
|
|
1232
|
+
// @fastify/multipart v10 exposes files via async iterators (request.parts()), not req.body.
|
|
945
1233
|
let bodyVarName = 'req.body';
|
|
946
1234
|
if (op.bodyInfo !== undefined) {
|
|
947
1235
|
if (op.bodyInfo.contentType === 'multipart/form-data') {
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
|
|
1236
|
+
// multipart/form-data: @fastify/multipart must be registered with attachFieldsToBody: true.
|
|
1237
|
+
// Without that option, files are only accessible via async iterators (request.parts()),
|
|
1238
|
+
// NOT via req.body. The consumer must register the plugin before creating this router.
|
|
1239
|
+
lines.push(`${indent} // multipart/form-data: requires @fastify/multipart registered with { attachFieldsToBody: true }.`);
|
|
1240
|
+
// bodyVarName stays 'req.body'
|
|
1241
|
+
}
|
|
1242
|
+
else if (op.bodyInfo.contentType === 'application/octet-stream') {
|
|
1243
|
+
// application/octet-stream: req.body is a Buffer (parsed by the addContentTypeParser
|
|
1244
|
+
// registration emitted in the createRouter body above). Forward it to the service as-is.
|
|
1245
|
+
lines.push(`${indent} // application/octet-stream: req.body is a Buffer from the registered content-type parser.`);
|
|
951
1246
|
// bodyVarName stays 'req.body'
|
|
952
1247
|
}
|
|
953
1248
|
else {
|
|
@@ -957,9 +1252,18 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
957
1252
|
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
958
1253
|
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
|
|
959
1254
|
lines.push(`${indent} if (!parseResult.success) {`);
|
|
960
|
-
lines.push(`${indent} return reply.status(422).send({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
1255
|
+
lines.push(`${indent} return (reply as FastifyReply).status(422).send({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
961
1256
|
lines.push(`${indent} }`);
|
|
962
|
-
|
|
1257
|
+
// Forward the validated/coerced data (parseResult.data), NOT the raw req.body,
|
|
1258
|
+
// so Zod coercion is preserved (e.g. form-urlencoded numeric fields via
|
|
1259
|
+
// z.coerce.number()). Cast to the declared model type so the service-call type
|
|
1260
|
+
// stays correct even when the schema infers a narrower shape (e.g. z.unknown()
|
|
1261
|
+
// for inline-union properties); safeParse above guarantees runtime safety.
|
|
1262
|
+
const bodyCastType = op.bodyInfo.typeName !== undefined && !op.bodyInfo.isSynthesized
|
|
1263
|
+
? op.bodyInfo.typeName
|
|
1264
|
+
: 'unknown';
|
|
1265
|
+
lines.push(`${indent} const validatedBody = parseResult.data as ${bodyCastType}`);
|
|
1266
|
+
bodyVarName = 'validatedBody';
|
|
963
1267
|
}
|
|
964
1268
|
}
|
|
965
1269
|
}
|
|
@@ -972,7 +1276,15 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
972
1276
|
serviceArgs.push(bodyVarName);
|
|
973
1277
|
}
|
|
974
1278
|
if (op.queryParams.length > 0) {
|
|
975
|
-
|
|
1279
|
+
// After successful Zod validation _qv.data carries the correct required/typed
|
|
1280
|
+
// values (e.g. string[] for delimited arrays, object shape for deepObject params,
|
|
1281
|
+
// and non-optional values for required scalar params). Use _qv.data when validation
|
|
1282
|
+
// was applied; fall back to params when no validation is needed.
|
|
1283
|
+
serviceArgs.push(queryParamsNeedValidation(op.queryParams) ? '_qv.data' : 'params');
|
|
1284
|
+
}
|
|
1285
|
+
// Context arg: pass the Fastify Request object (req) as the final argument when contextType is set.
|
|
1286
|
+
if (contextType !== undefined) {
|
|
1287
|
+
serviceArgs.push('req');
|
|
976
1288
|
}
|
|
977
1289
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
978
1290
|
// Response — wrap in try/catch to map HttpError to its status
|
|
@@ -1003,15 +1315,15 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
1003
1315
|
}
|
|
1004
1316
|
}
|
|
1005
1317
|
else if (op.responseStatus.status === 200) {
|
|
1006
|
-
lines.push(`${indent} return ${serviceCall}`);
|
|
1318
|
+
lines.push(`${indent} return await ${serviceCall}`);
|
|
1007
1319
|
}
|
|
1008
1320
|
else {
|
|
1009
1321
|
lines.push(`${indent} reply.status(${op.responseStatus.status})`);
|
|
1010
|
-
lines.push(`${indent} return ${serviceCall}`);
|
|
1322
|
+
lines.push(`${indent} return await ${serviceCall}`);
|
|
1011
1323
|
}
|
|
1012
1324
|
lines.push(`${indent} } catch (err) {`);
|
|
1013
1325
|
lines.push(`${indent} if (err instanceof HttpError) {`);
|
|
1014
|
-
lines.push(`${indent} return reply.status(err.status).send({ error: err.message })`);
|
|
1326
|
+
lines.push(`${indent} return (reply as FastifyReply).status(err.status).send({ error: err.message })`);
|
|
1015
1327
|
lines.push(`${indent} }`);
|
|
1016
1328
|
lines.push(`${indent} throw err`);
|
|
1017
1329
|
lines.push(`${indent} }`);
|
|
@@ -1037,7 +1349,8 @@ function httpErrorClassLines() {
|
|
|
1037
1349
|
// ── Zod import helpers ────────────────────────────────────────────────────────
|
|
1038
1350
|
/**
|
|
1039
1351
|
* Returns true when any operation in the list generates param validation code
|
|
1040
|
-
* that requires Zod (path format validation, required/typed query params,
|
|
1352
|
+
* that requires Zod (path format validation, required/typed query params, header params,
|
|
1353
|
+
* or cookie params).
|
|
1041
1354
|
*/
|
|
1042
1355
|
function operationsNeedZodForParams(operations) {
|
|
1043
1356
|
for (const op of operations) {
|
|
@@ -1047,6 +1360,8 @@ function operationsNeedZodForParams(operations) {
|
|
|
1047
1360
|
return true;
|
|
1048
1361
|
if (op.headerParams.length > 0)
|
|
1049
1362
|
return true;
|
|
1363
|
+
if (op.cookieParams.length > 0)
|
|
1364
|
+
return true;
|
|
1050
1365
|
}
|
|
1051
1366
|
return false;
|
|
1052
1367
|
}
|
|
@@ -1056,6 +1371,7 @@ export function generateExpressRouter(spec, options) {
|
|
|
1056
1371
|
const serviceName = deriveServiceName(spec);
|
|
1057
1372
|
const operations = collectOperations(spec);
|
|
1058
1373
|
const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
1374
|
+
// usedResponseSchemaNames is Fastify-only; not used in Express generator.
|
|
1059
1375
|
const lines = [];
|
|
1060
1376
|
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
1061
1377
|
lines.push('// Express: apply express.json() middleware before mounting this router so req.body is populated.');
|
|
@@ -1065,6 +1381,8 @@ export function generateExpressRouter(spec, options) {
|
|
|
1065
1381
|
if (sortedBodyTypes.length > 0) {
|
|
1066
1382
|
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
1067
1383
|
}
|
|
1384
|
+
const ctx = options?.contextType;
|
|
1385
|
+
const serviceRef = ctx !== undefined ? `${serviceName}<${ctx}>` : serviceName;
|
|
1068
1386
|
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
1069
1387
|
if (needsZod) {
|
|
1070
1388
|
lines.push(`import { z } from 'zod'`);
|
|
@@ -1077,11 +1395,11 @@ export function generateExpressRouter(spec, options) {
|
|
|
1077
1395
|
for (const l of httpErrorClassLines())
|
|
1078
1396
|
lines.push(l);
|
|
1079
1397
|
lines.push('');
|
|
1080
|
-
lines.push(`export function createRouter(service: ${
|
|
1398
|
+
lines.push(`export function createRouter(service: ${serviceRef}): Router {`);
|
|
1081
1399
|
lines.push(' const router = Router()');
|
|
1082
1400
|
lines.push('');
|
|
1083
1401
|
for (const op of operations) {
|
|
1084
|
-
lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames));
|
|
1402
|
+
lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames, ctx));
|
|
1085
1403
|
lines.push('');
|
|
1086
1404
|
}
|
|
1087
1405
|
lines.push(' return router');
|
|
@@ -1097,11 +1415,20 @@ export function generateExpressRouter(spec, options) {
|
|
|
1097
1415
|
export function generateFastifyRouter(spec, options) {
|
|
1098
1416
|
const serviceName = deriveServiceName(spec);
|
|
1099
1417
|
const operations = collectOperations(spec);
|
|
1100
|
-
const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
1418
|
+
const { sortedBodyTypes, usedSchemaNames, usedResponseSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
1419
|
+
// Merge body and response schema imports into a single sorted import list.
|
|
1420
|
+
const allUsedSchemaNames = new Set([...usedSchemaNames, ...usedResponseSchemaNames]);
|
|
1421
|
+
// Detect operations that need the octet-stream request body parser.
|
|
1422
|
+
const hasOctetStreamRequestBody = operations.some((op) => op.bodyInfo?.contentType === 'application/octet-stream');
|
|
1101
1423
|
const lines = [];
|
|
1102
1424
|
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
1425
|
+
lines.push('// Fastify natively parses only application/json and text/plain request bodies.');
|
|
1426
|
+
lines.push('// For application/x-www-form-urlencoded bodies, register @fastify/formbody before this router.');
|
|
1427
|
+
lines.push('// For multipart/form-data bodies, register @fastify/multipart with { attachFieldsToBody: true } before this router.');
|
|
1103
1428
|
lines.push('');
|
|
1104
|
-
|
|
1429
|
+
const ctx = options?.contextType;
|
|
1430
|
+
const serviceRef = ctx !== undefined ? `${serviceName}<${ctx}>` : serviceName;
|
|
1431
|
+
lines.push("import type { FastifyInstance, FastifyReply } from 'fastify'");
|
|
1105
1432
|
if (sortedBodyTypes.length > 0) {
|
|
1106
1433
|
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
1107
1434
|
}
|
|
@@ -1109,18 +1436,46 @@ export function generateFastifyRouter(spec, options) {
|
|
|
1109
1436
|
if (needsZod) {
|
|
1110
1437
|
lines.push(`import { z } from 'zod'`);
|
|
1111
1438
|
}
|
|
1112
|
-
if (
|
|
1113
|
-
const sortedUsedSchemas = Array.from(
|
|
1439
|
+
if (allUsedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
1440
|
+
const sortedUsedSchemas = Array.from(allUsedSchemaNames).sort();
|
|
1114
1441
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
1115
1442
|
}
|
|
1116
1443
|
lines.push('');
|
|
1444
|
+
// Augment FastifyContextConfig so that config: { operationId } on each route is type-safe.
|
|
1445
|
+
// Without this, TypeScript rejects the operationId property because FastifyContextConfig
|
|
1446
|
+
// is empty by default. The augmentation is scoped to the generated router module.
|
|
1447
|
+
lines.push("declare module 'fastify' {");
|
|
1448
|
+
lines.push(' interface FastifyContextConfig {');
|
|
1449
|
+
lines.push(' operationId?: string');
|
|
1450
|
+
lines.push(' }');
|
|
1451
|
+
lines.push('}');
|
|
1452
|
+
lines.push('');
|
|
1117
1453
|
for (const l of httpErrorClassLines())
|
|
1118
1454
|
lines.push(l);
|
|
1119
1455
|
lines.push('');
|
|
1120
|
-
lines.push(`export function createRouter(app: FastifyInstance, service: ${
|
|
1456
|
+
lines.push(`export function createRouter(app: FastifyInstance, service: ${serviceRef}): void {`);
|
|
1457
|
+
// Register a built-in content-type parser for application/octet-stream so that
|
|
1458
|
+
// req.body is a Buffer. Fastify 5 does not parse octet-stream natively; without
|
|
1459
|
+
// this registration the framework returns a 415 before the handler runs.
|
|
1460
|
+
// addContentTypeParser is a core Fastify API and requires no extra dependency.
|
|
1461
|
+
if (hasOctetStreamRequestBody) {
|
|
1462
|
+
lines.push(" app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (req, body, done) => done(null, body))");
|
|
1463
|
+
}
|
|
1464
|
+
// Wire a Zod-aware serializer compiler when response schemas are present.
|
|
1465
|
+
// Fastify's default serializer does not understand Zod schemas; this compiler
|
|
1466
|
+
// detects any Zod schema by its .parse method and validates before serializing.
|
|
1467
|
+
if (usedResponseSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
1468
|
+
lines.push(' app.setSerializerCompiler(({ schema }) => {');
|
|
1469
|
+
lines.push(" if (schema !== null && typeof schema === 'object' && 'parse' in schema) {");
|
|
1470
|
+
lines.push(' const zodSchema = schema as { parse: (data: unknown) => unknown }');
|
|
1471
|
+
lines.push(' return (data: unknown) => JSON.stringify(zodSchema.parse(data))');
|
|
1472
|
+
lines.push(' }');
|
|
1473
|
+
lines.push(' return (data: unknown) => JSON.stringify(data)');
|
|
1474
|
+
lines.push(' })');
|
|
1475
|
+
}
|
|
1121
1476
|
for (const op of operations) {
|
|
1122
1477
|
lines.push('');
|
|
1123
|
-
lines.push(buildFastifyRouteHandler(op, ' ', options?.schemaNames));
|
|
1478
|
+
lines.push(buildFastifyRouteHandler(op, ' ', options?.schemaNames, ctx));
|
|
1124
1479
|
}
|
|
1125
1480
|
lines.push('}');
|
|
1126
1481
|
lines.push('');
|
|
@@ -1138,7 +1493,14 @@ export function generateRouter(spec, options) {
|
|
|
1138
1493
|
const lines = [];
|
|
1139
1494
|
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
1140
1495
|
lines.push('');
|
|
1496
|
+
const ctx = options?.contextType;
|
|
1497
|
+
const serviceRef = ctx !== undefined ? `${serviceName}<${ctx}>` : serviceName;
|
|
1498
|
+
// getCookie from hono/cookie is needed when any operation declares cookie params.
|
|
1499
|
+
const needsGetCookie = operations.some((op) => op.cookieParams.length > 0);
|
|
1141
1500
|
lines.push("import { Hono } from 'hono'");
|
|
1501
|
+
if (needsGetCookie) {
|
|
1502
|
+
lines.push("import { getCookie } from 'hono/cookie'");
|
|
1503
|
+
}
|
|
1142
1504
|
if (sortedBodyTypes.length > 0) {
|
|
1143
1505
|
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
1144
1506
|
}
|
|
@@ -1154,11 +1516,11 @@ export function generateRouter(spec, options) {
|
|
|
1154
1516
|
for (const l of httpErrorClassLines())
|
|
1155
1517
|
lines.push(l);
|
|
1156
1518
|
lines.push('');
|
|
1157
|
-
lines.push(`export function createRouter(service: ${
|
|
1519
|
+
lines.push(`export function createRouter(service: ${serviceRef}): Hono {`);
|
|
1158
1520
|
lines.push(' const app = new Hono()');
|
|
1159
1521
|
lines.push('');
|
|
1160
1522
|
for (const op of operations) {
|
|
1161
|
-
lines.push(buildRouteHandler(op, ' ', options?.schemaNames));
|
|
1523
|
+
lines.push(buildRouteHandler(op, ' ', options?.schemaNames, ctx));
|
|
1162
1524
|
lines.push('');
|
|
1163
1525
|
}
|
|
1164
1526
|
lines.push(' return app');
|