@codewithagents/openapi-server 1.3.1 → 1.4.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 +4 -4
- package/dist/cli-args.d.ts +23 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +37 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.cjs +787 -321
- package/dist/plugins/router.d.ts.map +1 -1
- package/dist/plugins/router.js +406 -111
- package/dist/plugins/router.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/plugins/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/plugins/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAgiBhE,UAAU,aAAa;IACrB,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AA8ZD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,CAAC,QAAQ,EAC1B,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CA0Cf;AAKD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,CAAC,QAAQ,EAC1B,OAAO,CAAC,EAAE,aAAa,GACtB,aAAa,CAmCf;AAKD,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,aAAa,CAsCjG"}
|
package/dist/plugins/router.js
CHANGED
|
@@ -43,7 +43,7 @@ function deriveServiceName(spec) {
|
|
|
43
43
|
.join('');
|
|
44
44
|
if (pascal.length === 0)
|
|
45
45
|
return 'ApiService';
|
|
46
|
-
// Guard against numeric-start identifiers (e.g. '1Password Connect'
|
|
46
|
+
// Guard against numeric-start identifiers (e.g. '1Password Connect' -> '_1PasswordConnect')
|
|
47
47
|
const safePascal = /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
|
|
48
48
|
if (safePascal.endsWith('Service'))
|
|
49
49
|
return safePascal;
|
|
@@ -53,14 +53,14 @@ function deriveServiceName(spec) {
|
|
|
53
53
|
* Converts a raw operationId into a valid camelCase JS identifier.
|
|
54
54
|
* Handles kebab-case, snake_case, dots, spaces, parens, braces and other
|
|
55
55
|
* non-alphanumeric separators found in real-world OpenAPI specs.
|
|
56
|
-
* e.g. "post-applePay-sessions"
|
|
57
|
-
* e.g. "calendar.calendars.insert"
|
|
58
|
-
* e.g. "Get User Profile"
|
|
59
|
-
* e.g. "forgotPassword(oneTimeCode)"
|
|
56
|
+
* e.g. "post-applePay-sessions" -> "postApplePaySessions"
|
|
57
|
+
* e.g. "calendar.calendars.insert" -> "calendarCalendarsInsert"
|
|
58
|
+
* e.g. "Get User Profile" -> "getUserProfile"
|
|
59
|
+
* e.g. "forgotPassword(oneTimeCode)" -> "forgotPasswordOneTimeCode"
|
|
60
60
|
*/
|
|
61
61
|
function sanitizeOperationId(id) {
|
|
62
62
|
const parts = id
|
|
63
|
-
.replace(/'/g, '') // strip apostrophes without splitting ("user's"
|
|
63
|
+
.replace(/'/g, '') // strip apostrophes without splitting ("user's" -> "users")
|
|
64
64
|
.split(/[^a-zA-Z0-9]+/) // split on any non-alphanumeric sequence
|
|
65
65
|
.filter(Boolean);
|
|
66
66
|
if (parts.length === 0)
|
|
@@ -150,6 +150,225 @@ function getQueryParams(operation, spec) {
|
|
|
150
150
|
}
|
|
151
151
|
return result;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Map a schema format string to a Zod chain modifier.
|
|
155
|
+
* Returns an empty string when no specific format validation is needed.
|
|
156
|
+
*/
|
|
157
|
+
function formatToZodModifier(format) {
|
|
158
|
+
switch (format) {
|
|
159
|
+
case 'uuid':
|
|
160
|
+
return '.uuid()';
|
|
161
|
+
case 'email':
|
|
162
|
+
return '.email()';
|
|
163
|
+
case 'uri':
|
|
164
|
+
case 'url':
|
|
165
|
+
return '.url()';
|
|
166
|
+
case 'date-time':
|
|
167
|
+
return '.datetime()';
|
|
168
|
+
default:
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build a Zod expression for a path parameter based on its schema.
|
|
174
|
+
* Returns undefined when the parameter does not need format validation
|
|
175
|
+
* (simple string with no format constraint or non-string types used as strings in URLs).
|
|
176
|
+
*/
|
|
177
|
+
function pathParamZodExpr(schema) {
|
|
178
|
+
if (schema === undefined || isRef(schema))
|
|
179
|
+
return undefined;
|
|
180
|
+
const s = schema;
|
|
181
|
+
if (s.type !== 'string')
|
|
182
|
+
return undefined;
|
|
183
|
+
const format = s.format;
|
|
184
|
+
if (format === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const modifier = formatToZodModifier(format);
|
|
187
|
+
if (modifier === '')
|
|
188
|
+
return undefined;
|
|
189
|
+
return `z.string()${modifier}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build a Zod expression for a query or header parameter based on its schema.
|
|
193
|
+
* Number/integer types use z.number() (after coercion by extraction code).
|
|
194
|
+
* String types use z.string(). Boolean types use z.boolean().
|
|
195
|
+
* Appends .optional() for non-required params.
|
|
196
|
+
*/
|
|
197
|
+
function paramZodExpr(tsType, required, schema) {
|
|
198
|
+
let base;
|
|
199
|
+
if (tsType === 'number') {
|
|
200
|
+
base = 'z.number()';
|
|
201
|
+
}
|
|
202
|
+
else if (tsType === 'boolean') {
|
|
203
|
+
base = 'z.boolean()';
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// string
|
|
207
|
+
if (schema !== undefined && !isRef(schema)) {
|
|
208
|
+
const s = schema;
|
|
209
|
+
const format = s.format;
|
|
210
|
+
const modifier = format !== undefined ? formatToZodModifier(format) : '';
|
|
211
|
+
base = `z.string()${modifier}`;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
base = 'z.string()';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return required ? base : `${base}.optional()`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Collect path parameters that have Zod format constraints.
|
|
221
|
+
* Only returns entries for params that need format validation (e.g. uuid).
|
|
222
|
+
* Simple string params with no format constraint are excluded.
|
|
223
|
+
*/
|
|
224
|
+
function getPathParamValidations(operation, spec, rawPathParamNames) {
|
|
225
|
+
const parameters = operation.parameters;
|
|
226
|
+
if (parameters === undefined)
|
|
227
|
+
return [];
|
|
228
|
+
// Build a name-to-zodExpr map from path params to avoid nested loops.
|
|
229
|
+
const zodByName = new Map();
|
|
230
|
+
for (const p of parameters) {
|
|
231
|
+
const resolved = resolveParam(p, spec);
|
|
232
|
+
if (resolved === undefined || resolved.in !== 'path')
|
|
233
|
+
continue;
|
|
234
|
+
const schema = resolved.schema;
|
|
235
|
+
const zodExpr = pathParamZodExpr(schema);
|
|
236
|
+
if (zodExpr !== undefined)
|
|
237
|
+
zodByName.set(resolved.name, zodExpr);
|
|
238
|
+
}
|
|
239
|
+
const result = [];
|
|
240
|
+
for (const rawName of rawPathParamNames) {
|
|
241
|
+
const zodExpr = zodByName.get(rawName);
|
|
242
|
+
if (zodExpr !== undefined)
|
|
243
|
+
result.push({ rawName, zodExpr });
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Collect header parameters from an operation.
|
|
249
|
+
*/
|
|
250
|
+
function getHeaderParams(operation, spec) {
|
|
251
|
+
const parameters = operation.parameters;
|
|
252
|
+
if (parameters === undefined)
|
|
253
|
+
return [];
|
|
254
|
+
const result = [];
|
|
255
|
+
for (const p of parameters) {
|
|
256
|
+
const resolved = resolveParam(p, spec);
|
|
257
|
+
if (resolved === undefined || resolved.in !== 'header')
|
|
258
|
+
continue;
|
|
259
|
+
result.push({
|
|
260
|
+
rawName: resolved.name,
|
|
261
|
+
required: resolved.required === true,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Determine whether query params need a Zod validation block.
|
|
268
|
+
* Triggered when any param is required or has a non-string type (to catch NaN/invalid input).
|
|
269
|
+
*/
|
|
270
|
+
function queryParamsNeedValidation(queryParams) {
|
|
271
|
+
return queryParams.some((q) => q.required || q.tsType !== 'string');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Emit Zod validation lines for query parameters into the handler line buffer.
|
|
275
|
+
* Uses the already-extracted params object (after Number() coercion).
|
|
276
|
+
* Uses short variable name _qv to keep the 422 return line under Prettier's print width.
|
|
277
|
+
* @param indent - outer handler indent (e.g. ' ')
|
|
278
|
+
*/
|
|
279
|
+
function emitQueryValidation(lines, queryParams, indent) {
|
|
280
|
+
const inner = `${indent} `;
|
|
281
|
+
const fieldIndent = `${indent} `;
|
|
282
|
+
const fields = queryParams
|
|
283
|
+
.map((q) => {
|
|
284
|
+
const expr = paramZodExpr(q.tsType, q.required);
|
|
285
|
+
return `${fieldIndent}${q.name}: ${expr}`;
|
|
286
|
+
})
|
|
287
|
+
.join(',\n');
|
|
288
|
+
lines.push(`${inner}// Validate query parameters: returns 422 with Zod issues on failure`);
|
|
289
|
+
lines.push(`${inner}const _qv = z.object({`);
|
|
290
|
+
lines.push(fields);
|
|
291
|
+
lines.push(`${inner}}).safeParse(params)`);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Emit Zod validation lines for path parameters (format constraints) into the handler line buffer.
|
|
295
|
+
* Uses short variable name _pv to keep the 422 return line under Prettier's print width.
|
|
296
|
+
* @param indent - outer handler indent (e.g. ' ')
|
|
297
|
+
* @param framework - used to generate the correct param accessor syntax
|
|
298
|
+
*/
|
|
299
|
+
function emitPathValidation(lines, validations, indent, framework) {
|
|
300
|
+
const inner = `${indent} `;
|
|
301
|
+
const fieldIndent = `${indent} `;
|
|
302
|
+
const schemaFields = validations
|
|
303
|
+
.map((v) => {
|
|
304
|
+
const key = /[^a-zA-Z0-9_$]/.test(v.rawName) ? JSON.stringify(v.rawName) : v.rawName;
|
|
305
|
+
return `${fieldIndent}${key}: ${v.zodExpr}`;
|
|
306
|
+
})
|
|
307
|
+
.join(',\n');
|
|
308
|
+
const rawFields = validations
|
|
309
|
+
.map((v) => {
|
|
310
|
+
const key = /[^a-zA-Z0-9_$]/.test(v.rawName) ? JSON.stringify(v.rawName) : v.rawName;
|
|
311
|
+
let access;
|
|
312
|
+
if (framework === 'hono') {
|
|
313
|
+
access = `c.req.param(${JSON.stringify(v.rawName)})`;
|
|
314
|
+
}
|
|
315
|
+
else if (framework === 'express') {
|
|
316
|
+
access = `req.params[${JSON.stringify(v.rawName)}]`;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
access = /[^a-zA-Z0-9_$]/.test(v.rawName)
|
|
320
|
+
? `req.params[${JSON.stringify(v.rawName)}]`
|
|
321
|
+
: `req.params.${v.rawName}`;
|
|
322
|
+
}
|
|
323
|
+
return `${fieldIndent}${key}: ${access}`;
|
|
324
|
+
})
|
|
325
|
+
.join(',\n');
|
|
326
|
+
lines.push(`${inner}// Validate path parameters: returns 422 with Zod issues on failure`);
|
|
327
|
+
lines.push(`${inner}const _pv = z.object({`);
|
|
328
|
+
lines.push(schemaFields);
|
|
329
|
+
lines.push(`${inner}}).safeParse({`);
|
|
330
|
+
lines.push(rawFields);
|
|
331
|
+
lines.push(`${inner}})`);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Emit Zod validation lines for header parameters into the handler line buffer.
|
|
335
|
+
* Uses short variable name _hv to keep the 422 return line under Prettier's print width.
|
|
336
|
+
* @param indent - outer handler indent (e.g. ' ')
|
|
337
|
+
* @param framework - used to generate the correct header accessor syntax
|
|
338
|
+
*/
|
|
339
|
+
function emitHeaderValidation(lines, headerParams, indent, framework) {
|
|
340
|
+
const inner = `${indent} `;
|
|
341
|
+
const fieldIndent = `${indent} `;
|
|
342
|
+
const schemaFields = headerParams
|
|
343
|
+
.map((h) => {
|
|
344
|
+
const key = JSON.stringify(h.rawName);
|
|
345
|
+
const expr = h.required ? 'z.string()' : 'z.string().optional()';
|
|
346
|
+
return `${fieldIndent}${key}: ${expr}`;
|
|
347
|
+
})
|
|
348
|
+
.join(',\n');
|
|
349
|
+
const rawFields = headerParams
|
|
350
|
+
.map((h) => {
|
|
351
|
+
const key = JSON.stringify(h.rawName);
|
|
352
|
+
let access;
|
|
353
|
+
if (framework === 'hono') {
|
|
354
|
+
access = `c.req.header(${key})`;
|
|
355
|
+
}
|
|
356
|
+
else if (framework === 'express') {
|
|
357
|
+
access = `req.headers[${key}] as string | undefined`;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
access = `req.headers[${key}]`;
|
|
361
|
+
}
|
|
362
|
+
return `${fieldIndent}${key}: ${access}`;
|
|
363
|
+
})
|
|
364
|
+
.join(',\n');
|
|
365
|
+
lines.push(`${inner}// Validate request headers: returns 422 with Zod issues on failure`);
|
|
366
|
+
lines.push(`${inner}const _hv = z.object({`);
|
|
367
|
+
lines.push(schemaFields);
|
|
368
|
+
lines.push(`${inner}}).safeParse({`);
|
|
369
|
+
lines.push(rawFields);
|
|
370
|
+
lines.push(`${inner}})`);
|
|
371
|
+
}
|
|
153
372
|
function getBodyInfo(operation) {
|
|
154
373
|
const requestBody = operation.requestBody;
|
|
155
374
|
if (requestBody === undefined)
|
|
@@ -205,7 +424,9 @@ function collectOperations(spec) {
|
|
|
205
424
|
continue;
|
|
206
425
|
const methodName = deriveMethodName(operation.operationId, method, path);
|
|
207
426
|
const pathParams = extractPathParamsFromPath(path);
|
|
427
|
+
const pathParamValidations = getPathParamValidations(operation, spec, pathParams);
|
|
208
428
|
const queryParams = getQueryParams(operation, spec);
|
|
429
|
+
const headerParams = getHeaderParams(operation, spec);
|
|
209
430
|
const bodyInfo = getBodyInfo(operation);
|
|
210
431
|
const responseStatus = getResponseStatus(operation, method);
|
|
211
432
|
operations.push({
|
|
@@ -214,7 +435,9 @@ function collectOperations(spec) {
|
|
|
214
435
|
path,
|
|
215
436
|
honoPath: toHonoPath(path),
|
|
216
437
|
pathParams,
|
|
438
|
+
pathParamValidations,
|
|
217
439
|
queryParams,
|
|
440
|
+
headerParams,
|
|
218
441
|
bodyInfo,
|
|
219
442
|
responseStatus,
|
|
220
443
|
});
|
|
@@ -222,10 +445,53 @@ function collectOperations(spec) {
|
|
|
222
445
|
}
|
|
223
446
|
return operations;
|
|
224
447
|
}
|
|
448
|
+
/** Collect sorted body type names from all operations. */
|
|
449
|
+
function collectSortedBodyTypes(operations) {
|
|
450
|
+
const bodyTypes = new Set();
|
|
451
|
+
for (const op of operations) {
|
|
452
|
+
if (op.bodyInfo?.typeName !== undefined)
|
|
453
|
+
bodyTypes.add(op.bodyInfo.typeName);
|
|
454
|
+
}
|
|
455
|
+
return Array.from(bodyTypes).sort();
|
|
456
|
+
}
|
|
457
|
+
/** Collect the subset of schemaNames actually used by the given operations. */
|
|
458
|
+
function collectUsedSchemaNames(operations, schemaNames) {
|
|
459
|
+
const used = new Set();
|
|
460
|
+
for (const op of operations) {
|
|
461
|
+
const typeName = op.bodyInfo?.typeName;
|
|
462
|
+
if (typeName === undefined)
|
|
463
|
+
continue;
|
|
464
|
+
const schemaName = `${typeName}Schema`;
|
|
465
|
+
if (schemaNames.has(schemaName))
|
|
466
|
+
used.add(schemaName);
|
|
467
|
+
}
|
|
468
|
+
return used;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Collect body type names, used schema names, and whether Zod is needed.
|
|
472
|
+
* Shared by all three generator functions to avoid duplication.
|
|
473
|
+
*/
|
|
474
|
+
function collectGeneratorSetup(operations, options) {
|
|
475
|
+
const sortedBodyTypes = collectSortedBodyTypes(operations);
|
|
476
|
+
const usedSchemaNames = options?.schemaNames !== undefined
|
|
477
|
+
? collectUsedSchemaNames(operations, options.schemaNames)
|
|
478
|
+
: new Set();
|
|
479
|
+
const needsZod = (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) ||
|
|
480
|
+
operationsNeedZodForParams(operations);
|
|
481
|
+
return { sortedBodyTypes, usedSchemaNames, needsZod };
|
|
482
|
+
}
|
|
483
|
+
// ── Hono route handler ────────────────────────────────────────────────────────
|
|
225
484
|
// fallow-ignore-next-line complexity
|
|
226
485
|
function buildRouteHandler(op, indent, schemaNames) {
|
|
227
486
|
const lines = [];
|
|
228
|
-
lines.push(`${indent}app.${op.httpMethod}(
|
|
487
|
+
lines.push(`${indent}app.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (c) => {`);
|
|
488
|
+
// Path param format validation (e.g. uuid)
|
|
489
|
+
if (op.pathParamValidations.length > 0) {
|
|
490
|
+
emitPathValidation(lines, op.pathParamValidations, indent, 'hono');
|
|
491
|
+
lines.push(`${indent} if (!_pv.success) {`);
|
|
492
|
+
lines.push(`${indent} return c.json({ error: 'Invalid path parameters', issues: _pv.error.issues }, 422)`);
|
|
493
|
+
lines.push(`${indent} }`);
|
|
494
|
+
}
|
|
229
495
|
// Query params extraction
|
|
230
496
|
if (op.queryParams.length > 0) {
|
|
231
497
|
const fields = op.queryParams
|
|
@@ -239,6 +505,20 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
239
505
|
lines.push(`${indent} const params = {`);
|
|
240
506
|
lines.push(fields);
|
|
241
507
|
lines.push(`${indent} }`);
|
|
508
|
+
// Validate query params when there are required or typed (non-string) params
|
|
509
|
+
if (queryParamsNeedValidation(op.queryParams)) {
|
|
510
|
+
emitQueryValidation(lines, op.queryParams, indent);
|
|
511
|
+
lines.push(`${indent} if (!_qv.success) {`);
|
|
512
|
+
lines.push(`${indent} return c.json({ error: 'Invalid query parameters', issues: _qv.error.issues }, 422)`);
|
|
513
|
+
lines.push(`${indent} }`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Header param validation
|
|
517
|
+
if (op.headerParams.length > 0) {
|
|
518
|
+
emitHeaderValidation(lines, op.headerParams, indent, 'hono');
|
|
519
|
+
lines.push(`${indent} if (!_hv.success) {`);
|
|
520
|
+
lines.push(`${indent} return c.json({ error: 'Invalid request headers', issues: _hv.error.issues }, 422)`);
|
|
521
|
+
lines.push(`${indent} }`);
|
|
242
522
|
}
|
|
243
523
|
// Body extraction
|
|
244
524
|
let bodyVarName = 'body';
|
|
@@ -260,7 +540,7 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
260
540
|
// Build service call args
|
|
261
541
|
const serviceArgs = [];
|
|
262
542
|
for (const p of op.pathParams) {
|
|
263
|
-
serviceArgs.push(`c.req.param(
|
|
543
|
+
serviceArgs.push(`c.req.param(${JSON.stringify(p)})`);
|
|
264
544
|
}
|
|
265
545
|
if (op.bodyInfo !== undefined) {
|
|
266
546
|
serviceArgs.push(bodyVarName);
|
|
@@ -283,11 +563,18 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
283
563
|
lines.push(`${indent}})`);
|
|
284
564
|
return lines.join('\n');
|
|
285
565
|
}
|
|
286
|
-
// ── Express
|
|
566
|
+
// ── Express route handler ─────────────────────────────────────────────────────
|
|
287
567
|
// fallow-ignore-next-line complexity
|
|
288
568
|
function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
289
569
|
const lines = [];
|
|
290
|
-
lines.push(`${indent}router.${op.httpMethod}(
|
|
570
|
+
lines.push(`${indent}router.${op.httpMethod}(${JSON.stringify(op.honoPath)}, async (req: Request, res: Response) => {`);
|
|
571
|
+
// Path param format validation (e.g. uuid)
|
|
572
|
+
if (op.pathParamValidations.length > 0) {
|
|
573
|
+
emitPathValidation(lines, op.pathParamValidations, indent, 'express');
|
|
574
|
+
lines.push(`${indent} if (!_pv.success) {`);
|
|
575
|
+
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid path parameters', issues: _pv.error.issues })`);
|
|
576
|
+
lines.push(`${indent} }`);
|
|
577
|
+
}
|
|
291
578
|
// Query params extraction
|
|
292
579
|
if (op.queryParams.length > 0) {
|
|
293
580
|
const fields = op.queryParams
|
|
@@ -304,6 +591,20 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
304
591
|
lines.push(`${indent} const params = {`);
|
|
305
592
|
lines.push(fields);
|
|
306
593
|
lines.push(`${indent} }`);
|
|
594
|
+
// Validate query params when there are required or typed (non-string) params
|
|
595
|
+
if (queryParamsNeedValidation(op.queryParams)) {
|
|
596
|
+
emitQueryValidation(lines, op.queryParams, indent);
|
|
597
|
+
lines.push(`${indent} if (!_qv.success) {`);
|
|
598
|
+
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid query parameters', issues: _qv.error.issues })`);
|
|
599
|
+
lines.push(`${indent} }`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Header param validation
|
|
603
|
+
if (op.headerParams.length > 0) {
|
|
604
|
+
emitHeaderValidation(lines, op.headerParams, indent, 'express');
|
|
605
|
+
lines.push(`${indent} if (!_hv.success) {`);
|
|
606
|
+
lines.push(`${indent} return void res.status(422).json({ error: 'Invalid request headers', issues: _hv.error.issues })`);
|
|
607
|
+
lines.push(`${indent} }`);
|
|
307
608
|
}
|
|
308
609
|
// Body extraction, with optional Zod validation
|
|
309
610
|
let bodyVarName = 'body';
|
|
@@ -350,63 +651,7 @@ function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
|
350
651
|
lines.push(`${indent}})`);
|
|
351
652
|
return lines.join('\n');
|
|
352
653
|
}
|
|
353
|
-
//
|
|
354
|
-
export function generateExpressRouter(spec, options) {
|
|
355
|
-
const serviceName = deriveServiceName(spec);
|
|
356
|
-
const operations = collectOperations(spec);
|
|
357
|
-
// Collect body type names for import from models.js
|
|
358
|
-
const bodyTypes = new Set();
|
|
359
|
-
for (const op of operations) {
|
|
360
|
-
if (op.bodyInfo?.typeName !== undefined) {
|
|
361
|
-
bodyTypes.add(op.bodyInfo.typeName);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
const sortedBodyTypes = Array.from(bodyTypes).sort();
|
|
365
|
-
// Collect which schema names are actually needed (only for ops with a matching schema)
|
|
366
|
-
const usedSchemaNames = new Set();
|
|
367
|
-
if (options?.schemaNames !== undefined) {
|
|
368
|
-
for (const op of operations) {
|
|
369
|
-
const typeName = op.bodyInfo?.typeName;
|
|
370
|
-
if (typeName !== undefined) {
|
|
371
|
-
const schemaName = `${typeName}Schema`;
|
|
372
|
-
if (options.schemaNames.has(schemaName)) {
|
|
373
|
-
usedSchemaNames.add(schemaName);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const lines = [];
|
|
379
|
-
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
380
|
-
lines.push('// Express: apply express.json() middleware before mounting this router so req.body is populated.');
|
|
381
|
-
lines.push('');
|
|
382
|
-
lines.push("import { Router } from 'express'");
|
|
383
|
-
lines.push("import type { Request, Response } from 'express'");
|
|
384
|
-
if (sortedBodyTypes.length > 0) {
|
|
385
|
-
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
386
|
-
}
|
|
387
|
-
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
388
|
-
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
389
|
-
lines.push(`import { z } from 'zod'`);
|
|
390
|
-
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
391
|
-
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
392
|
-
}
|
|
393
|
-
lines.push('');
|
|
394
|
-
lines.push(`export function createRouter(service: ${serviceName}): Router {`);
|
|
395
|
-
lines.push(' const router = Router()');
|
|
396
|
-
lines.push('');
|
|
397
|
-
for (const op of operations) {
|
|
398
|
-
lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames));
|
|
399
|
-
lines.push('');
|
|
400
|
-
}
|
|
401
|
-
lines.push(' return router');
|
|
402
|
-
lines.push('}');
|
|
403
|
-
lines.push('');
|
|
404
|
-
return {
|
|
405
|
-
filename: 'router.ts',
|
|
406
|
-
content: lines.join('\n'),
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
// ── Fastify router generator ───────────────────────────────────────────────────
|
|
654
|
+
// ── Fastify route handler ─────────────────────────────────────────────────────
|
|
410
655
|
// fallow-ignore-next-line complexity
|
|
411
656
|
function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
412
657
|
const lines = [];
|
|
@@ -435,13 +680,43 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
435
680
|
genericParts.push(`Params: { ${paramFields} }`);
|
|
436
681
|
}
|
|
437
682
|
const generic = genericParts.length > 0 ? `<{ ${genericParts.join('; ')} }>` : '';
|
|
438
|
-
lines.push(`${indent}app.${op.httpMethod}${generic}(
|
|
683
|
+
lines.push(`${indent}app.${op.httpMethod}${generic}(${JSON.stringify(op.honoPath)}, async (req, reply) => {`);
|
|
684
|
+
// Path param format validation (e.g. uuid)
|
|
685
|
+
if (op.pathParamValidations.length > 0) {
|
|
686
|
+
emitPathValidation(lines, op.pathParamValidations, indent, 'fastify');
|
|
687
|
+
lines.push(`${indent} if (!_pv.success) {`);
|
|
688
|
+
lines.push(`${indent} return reply.status(422).send({`);
|
|
689
|
+
lines.push(`${indent} error: 'Invalid path parameters',`);
|
|
690
|
+
lines.push(`${indent} issues: _pv.error.issues,`);
|
|
691
|
+
lines.push(`${indent} })`);
|
|
692
|
+
lines.push(`${indent} }`);
|
|
693
|
+
}
|
|
439
694
|
// Query params extraction
|
|
440
695
|
if (op.queryParams.length > 0) {
|
|
441
696
|
const fields = op.queryParams.map((q) => ` ${q.name}: req.query.${q.name}`).join(',\n');
|
|
442
697
|
lines.push(`${indent} const params = {`);
|
|
443
698
|
lines.push(fields);
|
|
444
699
|
lines.push(`${indent} }`);
|
|
700
|
+
// Validate query params when there are required or typed (non-string) params
|
|
701
|
+
if (queryParamsNeedValidation(op.queryParams)) {
|
|
702
|
+
emitQueryValidation(lines, op.queryParams, indent);
|
|
703
|
+
lines.push(`${indent} if (!_qv.success) {`);
|
|
704
|
+
lines.push(`${indent} return reply.status(422).send({`);
|
|
705
|
+
lines.push(`${indent} error: 'Invalid query parameters',`);
|
|
706
|
+
lines.push(`${indent} issues: _qv.error.issues,`);
|
|
707
|
+
lines.push(`${indent} })`);
|
|
708
|
+
lines.push(`${indent} }`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Header param validation
|
|
712
|
+
if (op.headerParams.length > 0) {
|
|
713
|
+
emitHeaderValidation(lines, op.headerParams, indent, 'fastify');
|
|
714
|
+
lines.push(`${indent} if (!_hv.success) {`);
|
|
715
|
+
lines.push(`${indent} return reply.status(422).send({`);
|
|
716
|
+
lines.push(`${indent} error: 'Invalid request headers',`);
|
|
717
|
+
lines.push(`${indent} issues: _hv.error.issues,`);
|
|
718
|
+
lines.push(`${indent} })`);
|
|
719
|
+
lines.push(`${indent} }`);
|
|
445
720
|
}
|
|
446
721
|
// Body handling, with optional Zod validation
|
|
447
722
|
let bodyVarName = 'req.body';
|
|
@@ -484,31 +759,67 @@ function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
|
484
759
|
lines.push(`${indent}})`);
|
|
485
760
|
return lines.join('\n');
|
|
486
761
|
}
|
|
762
|
+
// ── Zod import helpers ────────────────────────────────────────────────────────
|
|
763
|
+
/**
|
|
764
|
+
* Returns true when any operation in the list generates param validation code
|
|
765
|
+
* that requires Zod (path format validation, required/typed query params, or header params).
|
|
766
|
+
*/
|
|
767
|
+
function operationsNeedZodForParams(operations) {
|
|
768
|
+
for (const op of operations) {
|
|
769
|
+
if (op.pathParamValidations.length > 0)
|
|
770
|
+
return true;
|
|
771
|
+
if (queryParamsNeedValidation(op.queryParams))
|
|
772
|
+
return true;
|
|
773
|
+
if (op.headerParams.length > 0)
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
// ── Express router generator ──────────────────────────────────────────────────
|
|
487
779
|
// fallow-ignore-next-line complexity
|
|
488
|
-
export function
|
|
780
|
+
export function generateExpressRouter(spec, options) {
|
|
489
781
|
const serviceName = deriveServiceName(spec);
|
|
490
782
|
const operations = collectOperations(spec);
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
783
|
+
const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
784
|
+
const lines = [];
|
|
785
|
+
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
786
|
+
lines.push('// Express: apply express.json() middleware before mounting this router so req.body is populated.');
|
|
787
|
+
lines.push('');
|
|
788
|
+
lines.push("import { Router } from 'express'");
|
|
789
|
+
lines.push("import type { Request, Response } from 'express'");
|
|
790
|
+
if (sortedBodyTypes.length > 0) {
|
|
791
|
+
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
497
792
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (options?.schemaNames !== undefined) {
|
|
502
|
-
for (const op of operations) {
|
|
503
|
-
const typeName = op.bodyInfo?.typeName;
|
|
504
|
-
if (typeName !== undefined) {
|
|
505
|
-
const schemaName = `${typeName}Schema`;
|
|
506
|
-
if (options.schemaNames.has(schemaName)) {
|
|
507
|
-
usedSchemaNames.add(schemaName);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
793
|
+
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
794
|
+
if (needsZod) {
|
|
795
|
+
lines.push(`import { z } from 'zod'`);
|
|
511
796
|
}
|
|
797
|
+
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
798
|
+
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
799
|
+
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
800
|
+
}
|
|
801
|
+
lines.push('');
|
|
802
|
+
lines.push(`export function createRouter(service: ${serviceName}): Router {`);
|
|
803
|
+
lines.push(' const router = Router()');
|
|
804
|
+
lines.push('');
|
|
805
|
+
for (const op of operations) {
|
|
806
|
+
lines.push(buildExpressRouteHandler(op, ' ', options?.schemaNames));
|
|
807
|
+
lines.push('');
|
|
808
|
+
}
|
|
809
|
+
lines.push(' return router');
|
|
810
|
+
lines.push('}');
|
|
811
|
+
lines.push('');
|
|
812
|
+
return {
|
|
813
|
+
filename: 'router.ts',
|
|
814
|
+
content: lines.join('\n'),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
// ── Fastify router generator ──────────────────────────────────────────────────
|
|
818
|
+
// fallow-ignore-next-line complexity
|
|
819
|
+
export function generateFastifyRouter(spec, options) {
|
|
820
|
+
const serviceName = deriveServiceName(spec);
|
|
821
|
+
const operations = collectOperations(spec);
|
|
822
|
+
const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
512
823
|
const lines = [];
|
|
513
824
|
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
514
825
|
lines.push('');
|
|
@@ -517,8 +828,10 @@ export function generateFastifyRouter(spec, options) {
|
|
|
517
828
|
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
518
829
|
}
|
|
519
830
|
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
520
|
-
if (
|
|
831
|
+
if (needsZod) {
|
|
521
832
|
lines.push(`import { z } from 'zod'`);
|
|
833
|
+
}
|
|
834
|
+
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
522
835
|
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
523
836
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
524
837
|
}
|
|
@@ -540,27 +853,7 @@ export function generateFastifyRouter(spec, options) {
|
|
|
540
853
|
export function generateRouter(spec, options) {
|
|
541
854
|
const serviceName = deriveServiceName(spec);
|
|
542
855
|
const operations = collectOperations(spec);
|
|
543
|
-
|
|
544
|
-
const bodyTypes = new Set();
|
|
545
|
-
for (const op of operations) {
|
|
546
|
-
if (op.bodyInfo?.typeName !== undefined) {
|
|
547
|
-
bodyTypes.add(op.bodyInfo.typeName);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
const sortedBodyTypes = Array.from(bodyTypes).sort();
|
|
551
|
-
// Collect which schema names are actually needed (only for ops with a matching schema)
|
|
552
|
-
const usedSchemaNames = new Set();
|
|
553
|
-
if (options?.schemaNames !== undefined) {
|
|
554
|
-
for (const op of operations) {
|
|
555
|
-
const typeName = op.bodyInfo?.typeName;
|
|
556
|
-
if (typeName !== undefined) {
|
|
557
|
-
const schemaName = `${typeName}Schema`;
|
|
558
|
-
if (options.schemaNames.has(schemaName)) {
|
|
559
|
-
usedSchemaNames.add(schemaName);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
856
|
+
const { sortedBodyTypes, usedSchemaNames, needsZod } = collectGeneratorSetup(operations, options);
|
|
564
857
|
const lines = [];
|
|
565
858
|
lines.push('// This file is auto-generated. Do not edit manually.');
|
|
566
859
|
lines.push('');
|
|
@@ -569,8 +862,10 @@ export function generateRouter(spec, options) {
|
|
|
569
862
|
lines.push(`import type { ${sortedBodyTypes.join(', ')} } from './models.js'`);
|
|
570
863
|
}
|
|
571
864
|
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
572
|
-
if (
|
|
865
|
+
if (needsZod) {
|
|
573
866
|
lines.push(`import { z } from 'zod'`);
|
|
867
|
+
}
|
|
868
|
+
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== undefined) {
|
|
574
869
|
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
575
870
|
lines.push(`import { ${sortedUsedSchemas.join(', ')} } from '${options.schemaImportPath}'`);
|
|
576
871
|
}
|