@codewithagents/openapi-server 1.8.0 → 1.9.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/dist/cli.cjs +551 -105
- package/dist/plugins/router.d.ts.map +1 -1
- package/dist/plugins/router.js +543 -93
- package/dist/plugins/router.js.map +1 -1
- package/dist/plugins/service.d.ts.map +1 -1
- package/dist/plugins/service.js +67 -24
- package/dist/plugins/service.js.map +1 -1
- package/dist/plugins/shared.d.ts +46 -0
- package/dist/plugins/shared.d.ts.map +1 -1
- package/dist/plugins/shared.js +122 -12
- package/dist/plugins/shared.js.map +1 -1
- package/package.json +2 -2
package/dist/plugins/router.js
CHANGED
|
@@ -24,13 +24,38 @@ function formatToZodModifier(format) {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Build a Zod expression for a path parameter based on its schema.
|
|
27
|
-
* Returns undefined when the parameter does not need
|
|
28
|
-
*
|
|
27
|
+
* Returns undefined when the parameter does not need validation.
|
|
28
|
+
*
|
|
29
|
+
* String params: validates format (uuid, email, url, date-time) via z.string().format().
|
|
30
|
+
* Integer/number params: validates range (minimum/maximum) via z.coerce.number().min().max().
|
|
31
|
+
* z.coerce.number() is used for path params because c.req.param() always returns a string;
|
|
32
|
+
* coercion converts the URL string to a number before the min/max check.
|
|
29
33
|
*/
|
|
30
34
|
function pathParamZodExpr(schema) {
|
|
31
35
|
if (schema === undefined || isRef(schema))
|
|
32
36
|
return undefined;
|
|
33
37
|
const s = schema;
|
|
38
|
+
// Integer / number path params with range constraints
|
|
39
|
+
if (s.type === 'integer' || s.type === 'number') {
|
|
40
|
+
const hasMin = typeof s.minimum === 'number';
|
|
41
|
+
const hasMax = typeof s.maximum === 'number';
|
|
42
|
+
const hasExcMin = typeof s.exclusiveMinimum === 'number';
|
|
43
|
+
const hasExcMax = typeof s.exclusiveMaximum === 'number';
|
|
44
|
+
if (hasMin || hasMax || hasExcMin || hasExcMax) {
|
|
45
|
+
let expr = 'z.coerce.number()';
|
|
46
|
+
if (hasMin)
|
|
47
|
+
expr += `.min(${s.minimum})`;
|
|
48
|
+
if (hasMax)
|
|
49
|
+
expr += `.max(${s.maximum})`;
|
|
50
|
+
if (hasExcMin)
|
|
51
|
+
expr += `.gt(${s.exclusiveMinimum})`;
|
|
52
|
+
if (hasExcMax)
|
|
53
|
+
expr += `.lt(${s.exclusiveMaximum})`;
|
|
54
|
+
return expr;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// String path params: only validated when a known format modifier exists
|
|
34
59
|
if (s.type !== 'string')
|
|
35
60
|
return undefined;
|
|
36
61
|
const format = s.format;
|
|
@@ -41,33 +66,102 @@ function pathParamZodExpr(schema) {
|
|
|
41
66
|
return undefined;
|
|
42
67
|
return `z.string()${modifier}`;
|
|
43
68
|
}
|
|
69
|
+
// ── queryParamZodExpr helpers (one per param kind) ───────────────────────────
|
|
70
|
+
/** Delimited array param: value has been split into string[] by the extraction layer. */
|
|
71
|
+
function queryParamDelimitedZodBase(_param) {
|
|
72
|
+
return 'z.array(z.string())';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* DeepObject param: assembled into Record<string, string>.
|
|
76
|
+
* Emits z.object({...}) with per-property coercion; all properties are .optional()
|
|
77
|
+
* because their presence is governed by the outer object's required flag.
|
|
78
|
+
*/
|
|
79
|
+
function queryParamDeepObjectZodBase(param) {
|
|
80
|
+
const propFields = (param.deepObjectProperties ?? []).map((p) => {
|
|
81
|
+
const coerced = p.tsType === 'number' ? 'z.coerce.number()' : 'z.string()';
|
|
82
|
+
return `${p.key}: ${coerced}.optional()`;
|
|
83
|
+
});
|
|
84
|
+
return `z.object({ ${propFields.join(', ')} })`;
|
|
85
|
+
}
|
|
86
|
+
/** Number/integer param: z.number() with optional range modifiers. */
|
|
87
|
+
function queryParamNumberZodBase(param) {
|
|
88
|
+
let base = 'z.number()';
|
|
89
|
+
if (param.minimum !== undefined)
|
|
90
|
+
base += `.min(${param.minimum})`;
|
|
91
|
+
if (param.maximum !== undefined)
|
|
92
|
+
base += `.max(${param.maximum})`;
|
|
93
|
+
if (param.exclusiveMinimum !== undefined)
|
|
94
|
+
base += `.gt(${param.exclusiveMinimum})`;
|
|
95
|
+
if (param.exclusiveMaximum !== undefined)
|
|
96
|
+
base += `.lt(${param.exclusiveMaximum})`;
|
|
97
|
+
return base;
|
|
98
|
+
}
|
|
99
|
+
/** String param: z.string() or z.enum([...]) with optional length/pattern modifiers. */
|
|
100
|
+
function queryParamStringZodBase(param) {
|
|
101
|
+
let base;
|
|
102
|
+
if (param.enum !== undefined && param.enum.length > 0) {
|
|
103
|
+
const members = param.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
104
|
+
base = `z.enum([${members}])`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
base = 'z.string()';
|
|
108
|
+
}
|
|
109
|
+
if (param.minLength !== undefined)
|
|
110
|
+
base += `.min(${param.minLength})`;
|
|
111
|
+
if (param.maxLength !== undefined)
|
|
112
|
+
base += `.max(${param.maxLength})`;
|
|
113
|
+
if (param.pattern !== undefined)
|
|
114
|
+
base += `.regex(/${param.pattern}/)`;
|
|
115
|
+
return base;
|
|
116
|
+
}
|
|
44
117
|
/**
|
|
45
|
-
* Build a Zod expression for a query
|
|
118
|
+
* Build a Zod expression for a query parameter based on its captured constraints.
|
|
46
119
|
* Number/integer types use z.number() (after coercion by extraction code).
|
|
47
|
-
* String types use z.string()
|
|
120
|
+
* String types use z.string() with optional format/enum/pattern/length modifiers.
|
|
121
|
+
* Delimited array params use z.array(z.string()).
|
|
122
|
+
* DeepObject params use z.object({...}) with per-property coercion.
|
|
48
123
|
* Appends .optional() for non-required params.
|
|
49
124
|
*/
|
|
50
|
-
function
|
|
125
|
+
function queryParamZodExpr(param) {
|
|
51
126
|
let base;
|
|
52
|
-
if (
|
|
53
|
-
base =
|
|
127
|
+
if (param.delimiterStyle !== undefined) {
|
|
128
|
+
base = queryParamDelimitedZodBase(param);
|
|
129
|
+
}
|
|
130
|
+
else if (param.isDeepObject === true && param.deepObjectProperties !== undefined) {
|
|
131
|
+
base = queryParamDeepObjectZodBase(param);
|
|
54
132
|
}
|
|
55
|
-
else if (tsType === '
|
|
133
|
+
else if (param.tsType === 'number') {
|
|
134
|
+
base = queryParamNumberZodBase(param);
|
|
135
|
+
}
|
|
136
|
+
else if (param.tsType === 'boolean') {
|
|
56
137
|
base = 'z.boolean()';
|
|
57
138
|
}
|
|
58
139
|
else {
|
|
59
|
-
|
|
60
|
-
if (schema !== undefined && !isRef(schema)) {
|
|
61
|
-
const s = schema;
|
|
62
|
-
const format = s.format;
|
|
63
|
-
const modifier = format !== undefined ? formatToZodModifier(format) : '';
|
|
64
|
-
base = `z.string()${modifier}`;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
base = 'z.string()';
|
|
68
|
-
}
|
|
140
|
+
base = queryParamStringZodBase(param);
|
|
69
141
|
}
|
|
70
|
-
return required ? base : `${base}.optional()`;
|
|
142
|
+
return param.required ? base : `${base}.optional()`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build a Zod expression for a header parameter based on its captured constraints.
|
|
146
|
+
* Header values are always strings; emits z.string() or z.enum([...]) with optional
|
|
147
|
+
* pattern/length modifiers. Appends .optional() for non-required params.
|
|
148
|
+
*/
|
|
149
|
+
function headerParamZodExpr(param) {
|
|
150
|
+
let base;
|
|
151
|
+
if (param.enum !== undefined && param.enum.length > 0) {
|
|
152
|
+
const members = param.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
153
|
+
base = `z.enum([${members}])`;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
base = 'z.string()';
|
|
157
|
+
}
|
|
158
|
+
if (param.minLength !== undefined)
|
|
159
|
+
base += `.min(${param.minLength})`;
|
|
160
|
+
if (param.maxLength !== undefined)
|
|
161
|
+
base += `.max(${param.maxLength})`;
|
|
162
|
+
if (param.pattern !== undefined)
|
|
163
|
+
base += `.regex(/${param.pattern}/)`;
|
|
164
|
+
return param.required ? base : `${base}.optional()`;
|
|
71
165
|
}
|
|
72
166
|
/**
|
|
73
167
|
* Collect path parameters that have Zod format constraints.
|
|
@@ -98,7 +192,7 @@ function getPathParamValidations(operation, spec, rawPathParamNames) {
|
|
|
98
192
|
return result;
|
|
99
193
|
}
|
|
100
194
|
/**
|
|
101
|
-
* Collect header parameters from an operation.
|
|
195
|
+
* Collect header parameters from an operation, including schema constraints.
|
|
102
196
|
*/
|
|
103
197
|
function getHeaderParams(operation, spec) {
|
|
104
198
|
const parameters = operation.parameters;
|
|
@@ -109,19 +203,60 @@ function getHeaderParams(operation, spec) {
|
|
|
109
203
|
const resolved = resolveParam(p, spec);
|
|
110
204
|
if (resolved === undefined || resolved.in !== 'header')
|
|
111
205
|
continue;
|
|
112
|
-
|
|
206
|
+
const param = {
|
|
113
207
|
rawName: resolved.name,
|
|
114
208
|
required: resolved.required === true,
|
|
115
|
-
}
|
|
209
|
+
};
|
|
210
|
+
const schema = resolved.schema;
|
|
211
|
+
if (schema !== undefined && !isRef(schema)) {
|
|
212
|
+
const s = schema;
|
|
213
|
+
if (Array.isArray(s.enum))
|
|
214
|
+
param.enum = s.enum;
|
|
215
|
+
if (typeof s.minLength === 'number')
|
|
216
|
+
param.minLength = s.minLength;
|
|
217
|
+
if (typeof s.maxLength === 'number')
|
|
218
|
+
param.maxLength = s.maxLength;
|
|
219
|
+
if (typeof s.pattern === 'string')
|
|
220
|
+
param.pattern = s.pattern;
|
|
221
|
+
}
|
|
222
|
+
result.push(param);
|
|
116
223
|
}
|
|
117
224
|
return result;
|
|
118
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Returns true when a query param carries schema constraints beyond basic type/required.
|
|
228
|
+
* These constraints require a Zod validation block even if the param is optional or string.
|
|
229
|
+
*/
|
|
230
|
+
function queryParamHasConstraints(q) {
|
|
231
|
+
// Fields that, when defined, indicate schema constraints are present.
|
|
232
|
+
const constraintFields = [
|
|
233
|
+
q.enum,
|
|
234
|
+
q.minimum,
|
|
235
|
+
q.maximum,
|
|
236
|
+
q.exclusiveMinimum,
|
|
237
|
+
q.exclusiveMaximum,
|
|
238
|
+
q.minLength,
|
|
239
|
+
q.maxLength,
|
|
240
|
+
q.pattern,
|
|
241
|
+
q.delimiterStyle,
|
|
242
|
+
];
|
|
243
|
+
return constraintFields.some((f) => f !== undefined) || q.isDeepObject === true;
|
|
244
|
+
}
|
|
119
245
|
/**
|
|
120
246
|
* Determine whether query params need a Zod validation block.
|
|
121
|
-
* Triggered when any param is required
|
|
247
|
+
* Triggered when any param is required, has a non-string type (to catch NaN/invalid input),
|
|
248
|
+
* or carries schema constraints (enum, min/max, pattern, etc.).
|
|
122
249
|
*/
|
|
123
250
|
function queryParamsNeedValidation(queryParams) {
|
|
124
|
-
return queryParams.some((q) => q.required || q.tsType !== 'string');
|
|
251
|
+
return queryParams.some((q) => q.required || q.tsType !== 'string' || queryParamHasConstraints(q));
|
|
252
|
+
}
|
|
253
|
+
/** Returns the delimiter character for a delimited-style array query param. */
|
|
254
|
+
function delimiterChar(style) {
|
|
255
|
+
if (style === 'ssv')
|
|
256
|
+
return ' ';
|
|
257
|
+
if (style === 'psv')
|
|
258
|
+
return '|';
|
|
259
|
+
return ',';
|
|
125
260
|
}
|
|
126
261
|
/**
|
|
127
262
|
* Emit Zod validation lines for query parameters into the handler line buffer.
|
|
@@ -134,7 +269,7 @@ function emitQueryValidation(lines, queryParams, indent) {
|
|
|
134
269
|
const fieldIndent = `${indent} `;
|
|
135
270
|
const fields = queryParams
|
|
136
271
|
.map((q) => {
|
|
137
|
-
const expr =
|
|
272
|
+
const expr = queryParamZodExpr(q);
|
|
138
273
|
return `${fieldIndent}${q.name}: ${expr}`;
|
|
139
274
|
})
|
|
140
275
|
.join(',\n');
|
|
@@ -195,7 +330,7 @@ function emitHeaderValidation(lines, headerParams, indent, framework) {
|
|
|
195
330
|
const schemaFields = headerParams
|
|
196
331
|
.map((h) => {
|
|
197
332
|
const key = JSON.stringify(h.rawName);
|
|
198
|
-
const expr = h
|
|
333
|
+
const expr = headerParamZodExpr(h);
|
|
199
334
|
return `${fieldIndent}${key}: ${expr}`;
|
|
200
335
|
})
|
|
201
336
|
.join(',\n');
|
|
@@ -229,22 +364,84 @@ function response200IsVoid(resp) {
|
|
|
229
364
|
const content = r.content;
|
|
230
365
|
return content === undefined || Object.keys(content).length === 0;
|
|
231
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Detect the success response content type from a ResponseObject.
|
|
369
|
+
* Returns 'text/plain' or 'application/octet-stream' for non-JSON responses,
|
|
370
|
+
* or 'application/json' as the default.
|
|
371
|
+
*/
|
|
372
|
+
function detectResponseContentType(resp) {
|
|
373
|
+
if (isRef(resp))
|
|
374
|
+
return 'application/json';
|
|
375
|
+
const r = resp;
|
|
376
|
+
const content = r.content;
|
|
377
|
+
if (content === undefined)
|
|
378
|
+
return 'application/json';
|
|
379
|
+
if ('text/plain' in content)
|
|
380
|
+
return 'text/plain';
|
|
381
|
+
if ('application/octet-stream' in content)
|
|
382
|
+
return 'application/octet-stream';
|
|
383
|
+
return 'application/json';
|
|
384
|
+
}
|
|
232
385
|
function getResponseStatus(operation, httpMethod) {
|
|
233
386
|
const responses = operation.responses;
|
|
234
387
|
if (responses === undefined) {
|
|
235
|
-
return httpMethod === 'delete'
|
|
388
|
+
return httpMethod === 'delete'
|
|
389
|
+
? { status: 204, isVoid: true, responseContentType: 'application/json' }
|
|
390
|
+
: { status: 200, isVoid: false, responseContentType: 'application/json' };
|
|
391
|
+
}
|
|
392
|
+
// Multi-status: more than one 2xx response with a body (excluding 204/void).
|
|
393
|
+
// Must be checked before individual 200/201/204 branches so that e.g. 200+202
|
|
394
|
+
// is not absorbed by the responses['200'] early return.
|
|
395
|
+
// The handler selects the status at runtime via a { status, body } envelope.
|
|
396
|
+
const contentfulTwoxxKeys = Object.keys(responses)
|
|
397
|
+
.filter((k) => /^2\d\d$/.test(k) && k !== '204')
|
|
398
|
+
.sort();
|
|
399
|
+
if (contentfulTwoxxKeys.length > 1) {
|
|
400
|
+
return {
|
|
401
|
+
status: 200,
|
|
402
|
+
isVoid: false,
|
|
403
|
+
responseContentType: 'application/json',
|
|
404
|
+
isMultiStatus: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (responses['201'] !== undefined) {
|
|
408
|
+
return {
|
|
409
|
+
status: 201,
|
|
410
|
+
isVoid: false,
|
|
411
|
+
responseContentType: detectResponseContentType(responses['201']),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (responses['204'] !== undefined) {
|
|
415
|
+
return { status: 204, isVoid: true, responseContentType: 'application/json' };
|
|
236
416
|
}
|
|
237
|
-
if (responses['201'] !== undefined)
|
|
238
|
-
return { status: 201, isVoid: false };
|
|
239
|
-
if (responses['204'] !== undefined)
|
|
240
|
-
return { status: 204, isVoid: true };
|
|
241
417
|
if (responses['200'] !== undefined) {
|
|
242
|
-
if (response200IsVoid(responses['200']))
|
|
243
|
-
return { status: 204, isVoid: true };
|
|
244
|
-
|
|
418
|
+
if (response200IsVoid(responses['200'])) {
|
|
419
|
+
return { status: 204, isVoid: true, responseContentType: 'application/json' };
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
status: 200,
|
|
423
|
+
isVoid: false,
|
|
424
|
+
responseContentType: detectResponseContentType(responses['200']),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Single non-200/201/204 2xx declared: honor that exact status code.
|
|
428
|
+
const twoxxKeys = Object.keys(responses).filter((k) => /^2\d\d$/.test(k) && k !== '200' && k !== '201' && k !== '204');
|
|
429
|
+
if (twoxxKeys.length === 1) {
|
|
430
|
+
const code = parseInt(twoxxKeys[0], 10);
|
|
431
|
+
const resp = responses[twoxxKeys[0]];
|
|
432
|
+
const isVoid = isRef(resp)
|
|
433
|
+
? false
|
|
434
|
+
: (() => {
|
|
435
|
+
const r = resp;
|
|
436
|
+
const content = r.content;
|
|
437
|
+
return content === undefined || Object.keys(content).length === 0;
|
|
438
|
+
})();
|
|
439
|
+
return { status: code, isVoid, responseContentType: detectResponseContentType(resp) };
|
|
245
440
|
}
|
|
246
441
|
// Default: delete -> 204, otherwise 200
|
|
247
|
-
return httpMethod === 'delete'
|
|
442
|
+
return httpMethod === 'delete'
|
|
443
|
+
? { status: 204, isVoid: true, responseContentType: 'application/json' }
|
|
444
|
+
: { status: 200, isVoid: false, responseContentType: 'application/json' };
|
|
248
445
|
}
|
|
249
446
|
function collectOperations(spec) {
|
|
250
447
|
const paths = spec.paths;
|
|
@@ -279,12 +476,16 @@ function collectOperations(spec) {
|
|
|
279
476
|
}
|
|
280
477
|
return operations;
|
|
281
478
|
}
|
|
282
|
-
/** Collect sorted body type names from all operations.
|
|
479
|
+
/** Collect sorted body type names from all operations.
|
|
480
|
+
* Synthesized names (inline schema, no $ref) are excluded because they have no
|
|
481
|
+
* corresponding entry in models.ts and must not appear in the model import.
|
|
482
|
+
*/
|
|
283
483
|
function collectSortedBodyTypes(operations) {
|
|
284
484
|
const bodyTypes = new Set();
|
|
285
485
|
for (const op of operations) {
|
|
286
|
-
if (op.bodyInfo?.typeName !== undefined)
|
|
486
|
+
if (op.bodyInfo?.typeName !== undefined && !op.bodyInfo.isSynthesized) {
|
|
287
487
|
bodyTypes.add(op.bodyInfo.typeName);
|
|
488
|
+
}
|
|
288
489
|
}
|
|
289
490
|
return Array.from(bodyTypes).sort();
|
|
290
491
|
}
|
|
@@ -328,8 +529,30 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
328
529
|
}
|
|
329
530
|
// Query params extraction
|
|
330
531
|
if (op.queryParams.length > 0) {
|
|
532
|
+
// Emit deepObject assembly blocks before the params object.
|
|
533
|
+
// c.req.queries() returns Record<string, string[]> with raw bracket-notation keys.
|
|
534
|
+
const deepObjectParams = op.queryParams.filter((q) => q.isDeepObject === true);
|
|
535
|
+
if (deepObjectParams.length > 0) {
|
|
536
|
+
lines.push(`${indent} const _dq = c.req.queries()`);
|
|
537
|
+
for (const q of deepObjectParams) {
|
|
538
|
+
const prefixLen = q.rawName.length + 1; // e.g. 'filter['.length
|
|
539
|
+
const bracketPrefix = q.rawName + '[';
|
|
540
|
+
lines.push(`${indent} const ${q.name} = Object.fromEntries(`);
|
|
541
|
+
lines.push(`${indent} Object.entries(_dq).filter(([k]) => k.startsWith('${bracketPrefix}') && k.endsWith(']')).map(([k, vs]) => [k.slice(${prefixLen}, -1), vs[0]])`);
|
|
542
|
+
lines.push(`${indent} )`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
331
545
|
const fields = op.queryParams
|
|
332
546
|
.map((q) => {
|
|
547
|
+
if (q.isDeepObject === true) {
|
|
548
|
+
// Already assembled above as a local variable
|
|
549
|
+
return ` ${q.name}`;
|
|
550
|
+
}
|
|
551
|
+
if (q.delimiterStyle !== undefined) {
|
|
552
|
+
// Use rawName to match the actual URL query key (e.g. 'csv', 'ssv', 'psv').
|
|
553
|
+
const delim = JSON.stringify(delimiterChar(q.delimiterStyle));
|
|
554
|
+
return ` ${q.name}: c.req.query('${q.rawName}') !== undefined ? c.req.query('${q.rawName}')!.split(${delim}) : undefined`;
|
|
555
|
+
}
|
|
333
556
|
if (q.tsType === 'number') {
|
|
334
557
|
return ` ${q.name}: c.req.query('${q.name}') !== undefined ? Number(c.req.query('${q.name}')) : undefined`;
|
|
335
558
|
}
|
|
@@ -357,8 +580,43 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
357
580
|
// Body extraction
|
|
358
581
|
let bodyVarName = 'body';
|
|
359
582
|
if (op.bodyInfo !== undefined) {
|
|
360
|
-
|
|
361
|
-
|
|
583
|
+
// Synthesized names (inline schemas) are schema-only; the TS type is unknown.
|
|
584
|
+
const typeDecl = op.bodyInfo.typeName !== undefined && !op.bodyInfo.isSynthesized
|
|
585
|
+
? op.bodyInfo.typeName
|
|
586
|
+
: 'unknown';
|
|
587
|
+
if (op.bodyInfo.contentType === 'application/x-www-form-urlencoded') {
|
|
588
|
+
// Form-urlencoded: check Content-Type then decode with parseBody().
|
|
589
|
+
// Values arrive as strings; Zod coercion handles type conversion (e.g. z.coerce.number()).
|
|
590
|
+
lines.push(`${indent} const _ct = c.req.header('content-type') ?? ''`);
|
|
591
|
+
lines.push(`${indent} if (!_ct.toLowerCase().startsWith('application/x-www-form-urlencoded')) {`);
|
|
592
|
+
lines.push(`${indent} return c.json({ error: 'Unsupported Media Type' }, 415)`);
|
|
593
|
+
lines.push(`${indent} }`);
|
|
594
|
+
lines.push(`${indent} const body: unknown = await c.req.parseBody()`);
|
|
595
|
+
}
|
|
596
|
+
else if (op.bodyInfo.contentType === 'multipart/form-data') {
|
|
597
|
+
// Multipart: decode with parseBody({ all: true }) so repeated file fields arrive as arrays.
|
|
598
|
+
// File fields are web-standard File objects; text fields are strings.
|
|
599
|
+
// No manual Content-Type check needed: parseBody handles multipart natively in Hono.
|
|
600
|
+
lines.push(`${indent} // multipart/form-data: parseBody({ all: true }) collects repeated keys into arrays.`);
|
|
601
|
+
lines.push(`${indent} const body: unknown = await c.req.parseBody({ all: true })`);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
// JSON body: check Content-Type then parse with JSON.parse (not c.req.json()).
|
|
605
|
+
// c.req.text() + JSON.parse() is used instead of c.req.json() because Hono's
|
|
606
|
+
// c.req.json() silently returns null for an empty body instead of throwing,
|
|
607
|
+
// which would pass the try/catch and reach Zod as null, causing a 422 rather
|
|
608
|
+
// than the correct 400. JSON.parse('') always throws SyntaxError.
|
|
609
|
+
lines.push(`${indent} const _ct = c.req.header('content-type') ?? ''`);
|
|
610
|
+
lines.push(`${indent} if (!_ct.toLowerCase().startsWith('application/json')) {`);
|
|
611
|
+
lines.push(`${indent} return c.json({ error: 'Unsupported Media Type' }, 415)`);
|
|
612
|
+
lines.push(`${indent} }`);
|
|
613
|
+
lines.push(`${indent} let body: ${typeDecl}`);
|
|
614
|
+
lines.push(`${indent} try {`);
|
|
615
|
+
lines.push(`${indent} body = JSON.parse(await c.req.text()) as ${typeDecl}`);
|
|
616
|
+
lines.push(`${indent} } catch {`);
|
|
617
|
+
lines.push(`${indent} return c.json({ error: 'Invalid JSON body' }, 400)`);
|
|
618
|
+
lines.push(`${indent} }`);
|
|
619
|
+
}
|
|
362
620
|
// Zod validation when schema is available
|
|
363
621
|
const schemaName = op.bodyInfo.typeName !== undefined ? `${op.bodyInfo.typeName}Schema` : undefined;
|
|
364
622
|
if (schemaName !== undefined && schemaNames !== undefined && schemaNames.has(schemaName)) {
|
|
@@ -383,17 +641,47 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
383
641
|
serviceArgs.push('params');
|
|
384
642
|
}
|
|
385
643
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
386
|
-
// Response
|
|
644
|
+
// Response — wrap in try/catch to map HttpError to its status
|
|
645
|
+
lines.push(`${indent} try {`);
|
|
387
646
|
if (op.responseStatus.isVoid) {
|
|
388
|
-
lines.push(`${indent}
|
|
389
|
-
lines.push(`${indent}
|
|
647
|
+
lines.push(`${indent} await ${serviceCall}`);
|
|
648
|
+
lines.push(`${indent} return new Response(null, { status: ${op.responseStatus.status} })`);
|
|
390
649
|
}
|
|
391
|
-
else if (op.responseStatus.
|
|
392
|
-
|
|
650
|
+
else if (op.responseStatus.isMultiStatus === true) {
|
|
651
|
+
// Multi-status: service returns { status: number; body: T }; router forwards both.
|
|
652
|
+
lines.push(`${indent} const _envelope = await ${serviceCall}`);
|
|
653
|
+
lines.push(`${indent} return c.json(_envelope.body, _envelope.status as any)`);
|
|
654
|
+
}
|
|
655
|
+
else if (op.responseStatus.responseContentType === 'text/plain') {
|
|
656
|
+
if (op.responseStatus.status === 200) {
|
|
657
|
+
lines.push(`${indent} return c.text(await ${serviceCall})`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
lines.push(`${indent} return c.text(await ${serviceCall}, ${op.responseStatus.status})`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (op.responseStatus.responseContentType === 'application/octet-stream') {
|
|
664
|
+
if (op.responseStatus.status === 200) {
|
|
665
|
+
lines.push(`${indent} const _result = await ${serviceCall}`);
|
|
666
|
+
lines.push(`${indent} return new Response(_result, { headers: { 'content-type': 'application/octet-stream' } })`);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
lines.push(`${indent} const _result = await ${serviceCall}`);
|
|
670
|
+
lines.push(`${indent} return new Response(_result, { status: ${op.responseStatus.status}, headers: { 'content-type': 'application/octet-stream' } })`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else if (op.responseStatus.status === 200) {
|
|
674
|
+
lines.push(`${indent} return c.json(await ${serviceCall})`);
|
|
393
675
|
}
|
|
394
676
|
else {
|
|
395
|
-
lines.push(`${indent}
|
|
677
|
+
lines.push(`${indent} return c.json(await ${serviceCall}, ${op.responseStatus.status})`);
|
|
396
678
|
}
|
|
679
|
+
lines.push(`${indent} } catch (err) {`);
|
|
680
|
+
lines.push(`${indent} if (err instanceof HttpError) {`);
|
|
681
|
+
lines.push(`${indent} return new Response(JSON.stringify({ error: err.message }), { status: err.status, headers: { 'content-type': 'application/json' } })`);
|
|
682
|
+
lines.push(`${indent} }`);
|
|
683
|
+
lines.push(`${indent} throw err`);
|
|
684
|
+
lines.push(`${indent} }`);
|
|
397
685
|
lines.push(`${indent}})`);
|
|
398
686
|
return lines.join('\n');
|
|
399
687
|
}
|
|
@@ -411,8 +699,19 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
411
699
|
}
|
|
412
700
|
// Query params extraction
|
|
413
701
|
if (op.queryParams.length > 0) {
|
|
702
|
+
// Express (qs, extended:true) parses bracket-notation automatically:
|
|
703
|
+
// filter[gte]=10 → req.query.filter = { gte: '10' }.
|
|
704
|
+
// DeepObject params are already assembled; just cast the nested object.
|
|
414
705
|
const fields = op.queryParams
|
|
415
706
|
.map((q) => {
|
|
707
|
+
if (q.isDeepObject === true) {
|
|
708
|
+
// Express with qs: req.query['filter'] is already { gte: '10', lte: '20' }
|
|
709
|
+
return ` ${q.name}: (req.query['${q.rawName}'] ?? {}) as Record<string, string | undefined>`;
|
|
710
|
+
}
|
|
711
|
+
if (q.delimiterStyle !== undefined) {
|
|
712
|
+
const delim = JSON.stringify(delimiterChar(q.delimiterStyle));
|
|
713
|
+
return ` ${q.name}: typeof req.query['${q.rawName}'] === 'string' ? (req.query['${q.rawName}'] as string).split(${delim}) : undefined`;
|
|
714
|
+
}
|
|
416
715
|
if (q.tsType === 'number') {
|
|
417
716
|
return ` ${q.name}: Number(req.query['${q.name}'] as string)`;
|
|
418
717
|
}
|
|
@@ -440,23 +739,38 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
440
739
|
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request headers', issues: _hv.error.issues })`);
|
|
441
740
|
lines.push(`${indent} }`);
|
|
442
741
|
}
|
|
443
|
-
// Body extraction, with optional Zod validation
|
|
742
|
+
// Body extraction, with optional Zod validation.
|
|
743
|
+
// For both JSON and form-urlencoded bodies Express pre-populates req.body via middleware
|
|
744
|
+
// (express.json() for JSON, express.urlencoded() for form). The router just reads req.body.
|
|
745
|
+
// For multipart/form-data: assumes multer (or equivalent) middleware is applied before this
|
|
746
|
+
// router, populating req.files and req.body with the parsed multipart fields.
|
|
444
747
|
let bodyVarName = 'body';
|
|
445
748
|
if (op.bodyInfo !== undefined) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
lines.push(`${indent} //
|
|
450
|
-
lines.push(`${indent} const
|
|
451
|
-
lines.push(`${indent} if (!parseResult.success) {`);
|
|
452
|
-
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
453
|
-
lines.push(`${indent} }`);
|
|
454
|
-
lines.push(`${indent} const validatedBody = parseResult.data`);
|
|
455
|
-
bodyVarName = 'validatedBody';
|
|
749
|
+
if (op.bodyInfo.contentType === 'multipart/form-data') {
|
|
750
|
+
// Multipart assumption: multer middleware populates req.files (file fields) and
|
|
751
|
+
// req.body (text fields) before this handler runs. Merge them for service consumption.
|
|
752
|
+
lines.push(`${indent} // multipart/form-data: assumes multer middleware has populated req.files + req.body.`);
|
|
753
|
+
lines.push(`${indent} const body = { ...req.body, ...(req as any).files } as unknown`);
|
|
456
754
|
}
|
|
457
755
|
else {
|
|
458
|
-
const
|
|
459
|
-
|
|
756
|
+
const schemaName = op.bodyInfo.typeName !== undefined ? `${op.bodyInfo.typeName}Schema` : undefined;
|
|
757
|
+
const useZod = schemaName !== undefined && schemaNames !== undefined && schemaNames.has(schemaName);
|
|
758
|
+
if (useZod) {
|
|
759
|
+
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
760
|
+
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
|
|
761
|
+
lines.push(`${indent} if (!parseResult.success) {`);
|
|
762
|
+
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
763
|
+
lines.push(`${indent} }`);
|
|
764
|
+
lines.push(`${indent} const validatedBody = parseResult.data`);
|
|
765
|
+
bodyVarName = 'validatedBody';
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
// Synthesized names (inline schemas) have no model type — use plain cast to unknown.
|
|
769
|
+
const typeAnnotation = op.bodyInfo.typeName !== undefined && !op.bodyInfo.isSynthesized
|
|
770
|
+
? ` as ${op.bodyInfo.typeName}`
|
|
771
|
+
: '';
|
|
772
|
+
lines.push(`${indent} const body = req.body${typeAnnotation}`);
|
|
773
|
+
}
|
|
460
774
|
}
|
|
461
775
|
}
|
|
462
776
|
// Build service call args
|
|
@@ -471,17 +785,45 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
471
785
|
serviceArgs.push('params');
|
|
472
786
|
}
|
|
473
787
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
474
|
-
// Response
|
|
788
|
+
// Response — wrap in try/catch to map HttpError to its status
|
|
789
|
+
lines.push(`${indent} try {`);
|
|
475
790
|
if (op.responseStatus.isVoid) {
|
|
476
|
-
lines.push(`${indent}
|
|
477
|
-
lines.push(`${indent}
|
|
791
|
+
lines.push(`${indent} await ${serviceCall}`);
|
|
792
|
+
lines.push(`${indent} res.status(${op.responseStatus.status}).end()`);
|
|
478
793
|
}
|
|
479
|
-
else if (op.responseStatus.
|
|
480
|
-
|
|
794
|
+
else if (op.responseStatus.isMultiStatus === true) {
|
|
795
|
+
// Multi-status: service returns { status: number; body: T }; router forwards both.
|
|
796
|
+
lines.push(`${indent} const _envelope = await ${serviceCall}`);
|
|
797
|
+
lines.push(`${indent} res.status(_envelope.status).json(_envelope.body)`);
|
|
798
|
+
}
|
|
799
|
+
else if (op.responseStatus.responseContentType === 'text/plain') {
|
|
800
|
+
if (op.responseStatus.status === 200) {
|
|
801
|
+
lines.push(`${indent} res.type('text/plain').send(await ${serviceCall})`);
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
lines.push(`${indent} res.status(${op.responseStatus.status}).type('text/plain').send(await ${serviceCall})`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else if (op.responseStatus.responseContentType === 'application/octet-stream') {
|
|
808
|
+
if (op.responseStatus.status === 200) {
|
|
809
|
+
lines.push(`${indent} res.setHeader('Content-Type', 'application/octet-stream').send(Buffer.from(await ${serviceCall}))`);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
lines.push(`${indent} res.status(${op.responseStatus.status}).setHeader('Content-Type', 'application/octet-stream').send(Buffer.from(await ${serviceCall}))`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else if (op.responseStatus.status === 200) {
|
|
816
|
+
lines.push(`${indent} res.json(await ${serviceCall})`);
|
|
481
817
|
}
|
|
482
818
|
else {
|
|
483
|
-
lines.push(`${indent}
|
|
819
|
+
lines.push(`${indent} res.status(${op.responseStatus.status}).json(await ${serviceCall})`);
|
|
484
820
|
}
|
|
821
|
+
lines.push(`${indent} } catch (err) {`);
|
|
822
|
+
lines.push(`${indent} if (err instanceof HttpError) {`);
|
|
823
|
+
lines.push(`${indent} return void res.status(err.status).json({ error: err.message })`);
|
|
824
|
+
lines.push(`${indent} }`);
|
|
825
|
+
lines.push(`${indent} throw err`);
|
|
826
|
+
lines.push(`${indent} }`);
|
|
485
827
|
lines.push(`${indent}})`);
|
|
486
828
|
return lines.join('\n');
|
|
487
829
|
}
|
|
@@ -492,18 +834,30 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
492
834
|
// Build generic type argument
|
|
493
835
|
const genericParts = [];
|
|
494
836
|
if (op.queryParams.length > 0) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
837
|
+
// DeepObject and delimited params use bracket-notation keys or raw strings;
|
|
838
|
+
// include them as Record<string, string> or string[] in the Querystring generic.
|
|
839
|
+
const hasDeepOrDelimited = op.queryParams.some((q) => q.isDeepObject === true || q.delimiterStyle !== undefined);
|
|
840
|
+
let querystringType;
|
|
841
|
+
if (hasDeepOrDelimited) {
|
|
842
|
+
// Use a loose Querystring type that allows bracket-notation keys (fast-querystring stores
|
|
843
|
+
// them as literal strings, e.g. 'filter[gte]') and array values for delimited params.
|
|
844
|
+
querystringType = 'Record<string, string | string[] | undefined>';
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
const queryFields = op.queryParams
|
|
848
|
+
.map((q) => {
|
|
849
|
+
if (q.tsType === 'number')
|
|
850
|
+
return `${q.name}?: number`;
|
|
851
|
+
if (q.tsType === 'boolean')
|
|
852
|
+
return `${q.name}?: boolean`;
|
|
853
|
+
return `${q.name}?: string`;
|
|
854
|
+
})
|
|
855
|
+
.join('; ');
|
|
856
|
+
querystringType = `{ ${queryFields} }`;
|
|
857
|
+
}
|
|
858
|
+
genericParts.push(`Querystring: ${querystringType}`);
|
|
505
859
|
}
|
|
506
|
-
if (op.bodyInfo !== undefined && op.bodyInfo.typeName !== undefined) {
|
|
860
|
+
if (op.bodyInfo !== undefined && op.bodyInfo.typeName !== undefined && !op.bodyInfo.isSynthesized) {
|
|
507
861
|
genericParts.push(`Body: ${op.bodyInfo.typeName}`);
|
|
508
862
|
}
|
|
509
863
|
else if (op.bodyInfo !== undefined) {
|
|
@@ -527,7 +881,39 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
527
881
|
}
|
|
528
882
|
// Query params extraction
|
|
529
883
|
if (op.queryParams.length > 0) {
|
|
530
|
-
|
|
884
|
+
// fast-querystring (Fastify default) stores bracket-notation keys as literals:
|
|
885
|
+
// filter[gte]=10 → req.query['filter[gte]'] = '10'.
|
|
886
|
+
// DeepObject and delimited params need raw string access; emit _dq cast once.
|
|
887
|
+
const deepObjectParams = op.queryParams.filter((q) => q.isDeepObject === true);
|
|
888
|
+
const hasDeepOrDelimited = op.queryParams.some((q) => q.isDeepObject === true || q.delimiterStyle !== undefined);
|
|
889
|
+
if (hasDeepOrDelimited) {
|
|
890
|
+
lines.push(`${indent} const _dq = req.query as unknown as Record<string, string | undefined>`);
|
|
891
|
+
}
|
|
892
|
+
if (deepObjectParams.length > 0) {
|
|
893
|
+
for (const q of deepObjectParams) {
|
|
894
|
+
const prefixLen = q.rawName.length + 1; // e.g. 'filter['.length
|
|
895
|
+
const bracketPrefix = q.rawName + '[';
|
|
896
|
+
lines.push(`${indent} const ${q.name} = Object.fromEntries(`);
|
|
897
|
+
lines.push(`${indent} Object.entries(_dq).filter(([k]) => k.startsWith('${bracketPrefix}') && k.endsWith(']')).map(([k, v]) => [k.slice(${prefixLen}, -1), v])`);
|
|
898
|
+
lines.push(`${indent} )`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const fields = op.queryParams
|
|
902
|
+
.map((q) => {
|
|
903
|
+
if (q.isDeepObject === true) {
|
|
904
|
+
// Already assembled above as a local variable
|
|
905
|
+
return ` ${q.name}`;
|
|
906
|
+
}
|
|
907
|
+
if (q.delimiterStyle !== undefined) {
|
|
908
|
+
const delim = JSON.stringify(delimiterChar(q.delimiterStyle));
|
|
909
|
+
return ` ${q.name}: typeof _dq['${q.rawName}'] === 'string' ? _dq['${q.rawName}']!.split(${delim}) : undefined`;
|
|
910
|
+
}
|
|
911
|
+
// When _dq is defined, use it for consistent access; otherwise use typed req.query.
|
|
912
|
+
return hasDeepOrDelimited
|
|
913
|
+
? ` ${q.name}: _dq['${q.rawName}']`
|
|
914
|
+
: ` ${q.name}: req.query.${q.name}`;
|
|
915
|
+
})
|
|
916
|
+
.join(',\n');
|
|
531
917
|
lines.push(`${indent} const params = {`);
|
|
532
918
|
lines.push(fields);
|
|
533
919
|
lines.push(`${indent} }`);
|
|
@@ -552,18 +938,29 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
552
938
|
lines.push(`${indent} })`);
|
|
553
939
|
lines.push(`${indent} }`);
|
|
554
940
|
}
|
|
555
|
-
// Body handling, with optional Zod validation
|
|
941
|
+
// Body handling, with optional Zod validation.
|
|
942
|
+
// Fastify pre-parses req.body for both JSON and form-urlencoded bodies via plugins.
|
|
943
|
+
// For multipart/form-data: assumes @fastify/multipart (or equivalent) plugin is registered
|
|
944
|
+
// so that req.body contains the parsed fields and file parts.
|
|
556
945
|
let bodyVarName = 'req.body';
|
|
557
946
|
if (op.bodyInfo !== undefined) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
lines.push(`${indent} //
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
947
|
+
if (op.bodyInfo.contentType === 'multipart/form-data') {
|
|
948
|
+
// Multipart assumption: @fastify/multipart plugin has populated req.body before this
|
|
949
|
+
// handler runs. The body is forwarded to the service as-is.
|
|
950
|
+
lines.push(`${indent} // multipart/form-data: assumes @fastify/multipart plugin has populated req.body.`);
|
|
951
|
+
// bodyVarName stays 'req.body'
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
const schemaName = op.bodyInfo.typeName !== undefined ? `${op.bodyInfo.typeName}Schema` : undefined;
|
|
955
|
+
const useZod = schemaName !== undefined && schemaNames !== undefined && schemaNames.has(schemaName);
|
|
956
|
+
if (useZod) {
|
|
957
|
+
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
958
|
+
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
|
|
959
|
+
lines.push(`${indent} if (!parseResult.success) {`);
|
|
960
|
+
lines.push(`${indent} return reply.status(422).send({ error: 'Invalid request body', issues: parseResult.error.issues })`);
|
|
961
|
+
lines.push(`${indent} }`);
|
|
962
|
+
bodyVarName = 'parseResult.data';
|
|
963
|
+
}
|
|
567
964
|
}
|
|
568
965
|
}
|
|
569
966
|
// Build service call args
|
|
@@ -578,21 +975,65 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
578
975
|
serviceArgs.push('params');
|
|
579
976
|
}
|
|
580
977
|
const serviceCall = `service.${op.methodName}(${serviceArgs.join(', ')})`;
|
|
581
|
-
// Response
|
|
978
|
+
// Response — wrap in try/catch to map HttpError to its status
|
|
979
|
+
lines.push(`${indent} try {`);
|
|
582
980
|
if (op.responseStatus.isVoid) {
|
|
583
|
-
lines.push(`${indent}
|
|
584
|
-
lines.push(`${indent}
|
|
981
|
+
lines.push(`${indent} await ${serviceCall}`);
|
|
982
|
+
lines.push(`${indent} reply.status(${op.responseStatus.status}).send()`);
|
|
585
983
|
}
|
|
586
|
-
else if (op.responseStatus.
|
|
587
|
-
|
|
588
|
-
lines.push(`${indent}
|
|
984
|
+
else if (op.responseStatus.isMultiStatus === true) {
|
|
985
|
+
// Multi-status: service returns { status: number; body: T }; router forwards both.
|
|
986
|
+
lines.push(`${indent} const _envelope = await ${serviceCall}`);
|
|
987
|
+
lines.push(`${indent} return reply.status(_envelope.status).send(_envelope.body)`);
|
|
988
|
+
}
|
|
989
|
+
else if (op.responseStatus.responseContentType === 'text/plain') {
|
|
990
|
+
if (op.responseStatus.status === 200) {
|
|
991
|
+
lines.push(`${indent} return reply.type('text/plain').send(await ${serviceCall})`);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
lines.push(`${indent} return reply.status(${op.responseStatus.status}).type('text/plain').send(await ${serviceCall})`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else if (op.responseStatus.responseContentType === 'application/octet-stream') {
|
|
998
|
+
if (op.responseStatus.status === 200) {
|
|
999
|
+
lines.push(`${indent} return reply.type('application/octet-stream').send(Buffer.from(await ${serviceCall}))`);
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
lines.push(`${indent} return reply.status(${op.responseStatus.status}).type('application/octet-stream').send(Buffer.from(await ${serviceCall}))`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
else if (op.responseStatus.status === 200) {
|
|
1006
|
+
lines.push(`${indent} return ${serviceCall}`);
|
|
589
1007
|
}
|
|
590
1008
|
else {
|
|
591
|
-
lines.push(`${indent}
|
|
1009
|
+
lines.push(`${indent} reply.status(${op.responseStatus.status})`);
|
|
1010
|
+
lines.push(`${indent} return ${serviceCall}`);
|
|
592
1011
|
}
|
|
1012
|
+
lines.push(`${indent} } catch (err) {`);
|
|
1013
|
+
lines.push(`${indent} if (err instanceof HttpError) {`);
|
|
1014
|
+
lines.push(`${indent} return reply.status(err.status).send({ error: err.message })`);
|
|
1015
|
+
lines.push(`${indent} }`);
|
|
1016
|
+
lines.push(`${indent} throw err`);
|
|
1017
|
+
lines.push(`${indent} }`);
|
|
593
1018
|
lines.push(`${indent}})`);
|
|
594
1019
|
return lines.join('\n');
|
|
595
1020
|
}
|
|
1021
|
+
// ── HttpError class ───────────────────────────────────────────────────────────
|
|
1022
|
+
/**
|
|
1023
|
+
* Lines that emit the exported HttpError class into a generated router file.
|
|
1024
|
+
* Services throw `new HttpError(404, 'Not found')` and the generated router
|
|
1025
|
+
* catches it, returning the matching HTTP status instead of a generic 500.
|
|
1026
|
+
*/
|
|
1027
|
+
function httpErrorClassLines() {
|
|
1028
|
+
return [
|
|
1029
|
+
'export class HttpError extends Error {',
|
|
1030
|
+
' constructor(public readonly status: number, message: string) {',
|
|
1031
|
+
' super(message)',
|
|
1032
|
+
" this.name = 'HttpError'",
|
|
1033
|
+
' }',
|
|
1034
|
+
'}',
|
|
1035
|
+
];
|
|
1036
|
+
}
|
|
596
1037
|
// ── Zod import helpers ────────────────────────────────────────────────────────
|
|
597
1038
|
/**
|
|
598
1039
|
* Returns true when any operation in the list generates param validation code
|
|
@@ -633,6 +1074,9 @@ export function generateExpressRouter(spec, options) {
|
|
|
633
1074
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
634
1075
|
}
|
|
635
1076
|
lines.push('');
|
|
1077
|
+
for (const l of httpErrorClassLines())
|
|
1078
|
+
lines.push(l);
|
|
1079
|
+
lines.push('');
|
|
636
1080
|
lines.push(`export function createRouter(service: ${serviceName}): Router {`);
|
|
637
1081
|
lines.push(' const router = Router()');
|
|
638
1082
|
lines.push('');
|
|
@@ -670,6 +1114,9 @@ export function generateFastifyRouter(spec, options) {
|
|
|
670
1114
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
671
1115
|
}
|
|
672
1116
|
lines.push('');
|
|
1117
|
+
for (const l of httpErrorClassLines())
|
|
1118
|
+
lines.push(l);
|
|
1119
|
+
lines.push('');
|
|
673
1120
|
lines.push(`export function createRouter(app: FastifyInstance, service: ${serviceName}): void {`);
|
|
674
1121
|
for (const op of operations) {
|
|
675
1122
|
lines.push('');
|
|
@@ -704,6 +1151,9 @@ export function generateRouter(spec, options) {
|
|
|
704
1151
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
705
1152
|
}
|
|
706
1153
|
lines.push('');
|
|
1154
|
+
for (const l of httpErrorClassLines())
|
|
1155
|
+
lines.push(l);
|
|
1156
|
+
lines.push('');
|
|
707
1157
|
lines.push(`export function createRouter(service: ${serviceName}): Hono {`);
|
|
708
1158
|
lines.push(' const app = new Hono()');
|
|
709
1159
|
lines.push('');
|