@codewithagents/openapi-server 1.2.0 → 1.3.1
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 +55 -25
- package/dist/cli.cjs +298 -42
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -7
- package/dist/config.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +23 -6
- package/dist/generator.js.map +1 -1
- package/dist/plugins/router.d.ts +2 -0
- package/dist/plugins/router.d.ts.map +1 -1
- package/dist/plugins/router.js +296 -39
- package/dist/plugins/router.js.map +1 -1
- package/dist/plugins/service.d.ts.map +1 -1
- package/dist/plugins/service.js +19 -8
- package/dist/plugins/service.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# @codewithagents/openapi-server
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.com/package/@codewithagents/openapi-server)
|
|
4
|
-
[](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/codewithagents/openapi-zod-ts)
|
|
6
|
+
[](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/codeql.yml)
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
📖 **[Full documentation](https://openapi.codewithagents.de/openapi-server)**
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
10
|
+
Generate a typed service interface from your OpenAPI 3.x spec. Framework-agnostic by design: wire it to Hono, Express, Fastify, or any router you already use.
|
|
11
|
+
|
|
12
|
+
- **Framework-agnostic service interface**: `service.ts` is a plain TypeScript interface with no framework imports. Implement it however you want: Hono, Express, Fastify, Koa, plain `http`, Bun, Deno, or anything else.
|
|
13
|
+
- **Optional router scaffolding**: set `"framework": "hono"` and get a ready-to-mount Hono router as a starting point. Set `"framework": "none"` and wire the interface yourself. The generated code only ever imports what you already have.
|
|
14
|
+
- **Type-safe contract**: the compiler tells you if your implementation drifts from the spec. Add an endpoint in the spec and forget to implement it. TypeScript fails the build.
|
|
15
|
+
- **Prettier-clean output**: every generated file passes `prettier --check` out of the box.
|
|
16
|
+
- **OpenAPI 3.x**: 3.1.x primary target, 3.0.x best-effort. Full support for `$ref`, `allOf`, `anyOf`, `oneOf`, `nullable`.
|
|
17
|
+
- **TypeScript strict mode**: all output passes `strict: true`.
|
|
14
18
|
|
|
15
19
|
---
|
|
16
20
|
|
|
@@ -22,7 +26,7 @@ pnpm add -D @codewithagents/openapi-server
|
|
|
22
26
|
npm install -D @codewithagents/openapi-server
|
|
23
27
|
```
|
|
24
28
|
|
|
25
|
-
Requires [`@codewithagents/openapi-gen`](../openapi-gen)
|
|
29
|
+
Requires [`@codewithagents/openapi-gen`](../openapi-gen). Run both generators together.
|
|
26
30
|
|
|
27
31
|
---
|
|
28
32
|
|
|
@@ -48,8 +52,8 @@ npx openapi-server
|
|
|
48
52
|
|
|
49
53
|
| File | What it contains |
|
|
50
54
|
|---|---|
|
|
51
|
-
| `service.ts` | TypeScript interface
|
|
52
|
-
| `router.ts` | `createRouter(service)` factory
|
|
55
|
+
| `service.ts` | TypeScript interface, one method per API operation |
|
|
56
|
+
| `router.ts` | `createRouter(service)` factory, mounts every route on a Hono app |
|
|
53
57
|
|
|
54
58
|
Run `openapi-gen` first (or together) so `models.ts` exists before `service.ts` imports from it:
|
|
55
59
|
|
|
@@ -133,7 +137,7 @@ The router handles:
|
|
|
133
137
|
- Path params: `{id}` → `:id` (Hono style), extracted via `c.req.param()`
|
|
134
138
|
- Query params: extracted and typed (`string`, `number`, `boolean`)
|
|
135
139
|
- Request bodies: parsed via `c.req.json<T>()` with the correct model type
|
|
136
|
-
- Response status: `200` for GET, `201` for POST, `204` for DELETE
|
|
140
|
+
- Response status: `200` for GET, `201` for POST, `204` for DELETE, derived from your spec
|
|
137
141
|
|
|
138
142
|
---
|
|
139
143
|
|
|
@@ -203,23 +207,25 @@ serve({ fetch: app.fetch, port: 3001 })
|
|
|
203
207
|
|
|
204
208
|
## Config reference
|
|
205
209
|
|
|
210
|
+
See the [full configuration reference](https://openapi.codewithagents.de/openapi-server#configuration) in the docs for a detailed options table and the `--config` CLI flag.
|
|
211
|
+
|
|
206
212
|
`openapi-server.config.json`:
|
|
207
213
|
|
|
208
214
|
```json
|
|
209
215
|
{
|
|
210
|
-
"input_openapi": "./spec/api.json", // required
|
|
211
|
-
"output": "./generated", // required
|
|
212
|
-
"framework": "hono", // optional
|
|
213
|
-
"input_schema": "./generated/schemas.ts" // optional
|
|
216
|
+
"input_openapi": "./spec/api.json", // required: path to OpenAPI 3.x spec (JSON or YAML)
|
|
217
|
+
"output": "./generated", // required: directory to write generated files
|
|
218
|
+
"framework": "hono", // optional: router target (default: "hono")
|
|
219
|
+
"input_schema": "./generated/schemas.ts" // optional: Zod schema file for request validation
|
|
214
220
|
}
|
|
215
221
|
```
|
|
216
222
|
|
|
217
223
|
| Field | Required | Default | Description |
|
|
218
224
|
|---|---|---|---|
|
|
219
|
-
| `input_openapi` | Yes |
|
|
220
|
-
| `output` | Yes |
|
|
225
|
+
| `input_openapi` | Yes | n/a | Path to OpenAPI 3.x spec |
|
|
226
|
+
| `output` | Yes | n/a | Directory to write `service.ts` and `router.ts` |
|
|
221
227
|
| `framework` | No | `"hono"` | Router framework to generate. Use `"none"` to generate only `service.ts` |
|
|
222
|
-
| `input_schema` | No |
|
|
228
|
+
| `input_schema` | No | none | Path to user-owned Zod schema file. Enables server-side request validation (see below) |
|
|
223
229
|
|
|
224
230
|
Use `--config <path>` to point at a config file in a different location:
|
|
225
231
|
|
|
@@ -233,6 +239,8 @@ Relative paths in the config resolve from the config file's directory.
|
|
|
233
239
|
|
|
234
240
|
## Zod request validation (`input_schema`)
|
|
235
241
|
|
|
242
|
+
See the [Zod validation](https://openapi.codewithagents.de/openapi-server#zod-validation-input_schema) section in the docs for the two-pass generation flow and schema naming convention.
|
|
243
|
+
|
|
236
244
|
Point `input_schema` at the same `schemas.ts` you use with `@codewithagents/openapi-gen`. The server generator adds runtime validation to every route that receives a request body:
|
|
237
245
|
|
|
238
246
|
**Config:**
|
|
@@ -270,9 +278,9 @@ Invalid requests get a structured `422` response instead of reaching your servic
|
|
|
270
278
|
}
|
|
271
279
|
```
|
|
272
280
|
|
|
273
|
-
**Same schemas, both sides of the wire
|
|
281
|
+
**Same schemas, both sides of the wire**: `openapi-gen` validates outgoing requests in the browser; `openapi-server` validates incoming requests on the server. One `schemas.ts`, one source of truth.
|
|
274
282
|
|
|
275
|
-
**Drift detection
|
|
283
|
+
**Drift detection**: if schemas diverge from the spec (extra schema, missing schema), the generator warns to stderr. Builds still succeed; the warning is advisory.
|
|
276
284
|
|
|
277
285
|
---
|
|
278
286
|
|
|
@@ -280,11 +288,33 @@ Invalid requests get a structured `422` response instead of reaching your servic
|
|
|
280
288
|
|
|
281
289
|
`service.ts` has no framework imports at all. It is always generated, always framework-agnostic, and works with anything.
|
|
282
290
|
|
|
283
|
-
`router.ts` is optional and
|
|
291
|
+
`router.ts` is optional and supports:
|
|
284
292
|
|
|
285
293
|
| Value | What you get |
|
|
286
294
|
|---|---|
|
|
287
|
-
| `"none"` | Only `service.ts`. Wire the interface
|
|
288
|
-
| `"hono"` | `service.ts` + a ready-to-mount `router.ts` using [Hono](https://hono.dev).
|
|
295
|
+
| `"none"` | Only `service.ts`. Wire the interface yourself. |
|
|
296
|
+
| `"hono"` | `service.ts` + a ready-to-mount `router.ts` using [Hono](https://hono.dev). Includes optional Zod request validation via `input_schema`. |
|
|
297
|
+
| `"express"` | `service.ts` + a ready-to-mount `router.ts` using [Express](https://expressjs.com) `Router`. Apply `express.json()` middleware before mounting. |
|
|
298
|
+
| `"fastify"` | `service.ts` + a route-registering `router.ts` using [Fastify](https://fastify.dev). Routes are registered onto a `FastifyInstance`; see mount pattern below. |
|
|
299
|
+
|
|
300
|
+
The framework package must be in your own `dependencies`. This package adds nothing at runtime.
|
|
301
|
+
|
|
302
|
+
**Mounting patterns:**
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
// Hono
|
|
306
|
+
app.route('/api', createRouter(service))
|
|
307
|
+
|
|
308
|
+
// Express
|
|
309
|
+
app.use(express.json())
|
|
310
|
+
app.use('/api', createRouter(service))
|
|
311
|
+
|
|
312
|
+
// Fastify: createRouter registers routes onto the instance rather than returning one
|
|
313
|
+
fastify.register(async (instance) => { createRouter(instance, service) }, { prefix: '/api' })
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The `"none"` path is always available and keeps the zero-footprint promise: the generated code has no runtime dependencies that you did not already choose.
|
|
317
|
+
|
|
318
|
+
## Error handling and troubleshooting
|
|
289
319
|
|
|
290
|
-
|
|
320
|
+
The generated router does not wrap service calls in `try/catch`. Errors propagate to the framework's own error handler. See [Error handling](https://openapi.codewithagents.de/openapi-server#error-handling) in the docs for per-framework error handler examples and [Troubleshooting](https://openapi.codewithagents.de/openapi-server#troubleshooting) for common issues such as missing Zod validation or `Cannot find module './models.js'`.
|
package/dist/cli.cjs
CHANGED
|
@@ -18478,8 +18478,8 @@ async function loadConfig(cwd2, configPath) {
|
|
|
18478
18478
|
if (typeof config["output"] !== "string" || !config["output"]) {
|
|
18479
18479
|
throw new Error('Config missing required field: "output" (output directory)');
|
|
18480
18480
|
}
|
|
18481
|
-
if (config["framework"] !== void 0 && config["framework"] !== "hono" && config["framework"] !== "none") {
|
|
18482
|
-
throw new Error('"framework" must be
|
|
18481
|
+
if (config["framework"] !== void 0 && config["framework"] !== "hono" && config["framework"] !== "express" && config["framework"] !== "fastify" && config["framework"] !== "none") {
|
|
18482
|
+
throw new Error('"framework" must be one of: "hono", "express", "fastify", or "none"');
|
|
18483
18483
|
}
|
|
18484
18484
|
if (config["input_schema"] !== void 0 && (typeof config["input_schema"] !== "string" || !config["input_schema"])) {
|
|
18485
18485
|
throw new Error('"input_schema" must be a non-empty string path to your Zod schema file');
|
|
@@ -18505,8 +18505,14 @@ async function parseSpec(inputPath) {
|
|
|
18505
18505
|
|
|
18506
18506
|
// ../openapi-gen/dist/utils/naming.js
|
|
18507
18507
|
function toTypeName(name) {
|
|
18508
|
-
const
|
|
18509
|
-
|
|
18508
|
+
const parts = name.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
18509
|
+
if (parts.length === 0)
|
|
18510
|
+
return "_";
|
|
18511
|
+
const joined = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
|
|
18512
|
+
const prefixed = /^[^a-zA-Z_$]/.test(joined) ? `_${joined}` : joined;
|
|
18513
|
+
const titled = prefixed.replace(/^(.)/, (_, char) => char.toUpperCase());
|
|
18514
|
+
const safe = titled.replace(/[^a-zA-Z0-9_$]/g, "");
|
|
18515
|
+
return safe.length > 0 ? safe : "_";
|
|
18510
18516
|
}
|
|
18511
18517
|
|
|
18512
18518
|
// src/plugins/service.ts
|
|
@@ -18582,7 +18588,11 @@ function deriveOperationName(method, path) {
|
|
|
18582
18588
|
return prefix + parts.join("");
|
|
18583
18589
|
}
|
|
18584
18590
|
function normalizeParamName(name) {
|
|
18585
|
-
|
|
18591
|
+
const stripped = name.replace(/\[\]$/, "").replace(/'/g, "");
|
|
18592
|
+
const parts = stripped.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
18593
|
+
if (parts.length === 0) return "_";
|
|
18594
|
+
const camel = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
|
|
18595
|
+
return /^[^a-zA-Z_$]/.test(camel) ? `_${camel}` : camel;
|
|
18586
18596
|
}
|
|
18587
18597
|
function getQueryParams(operation, spec) {
|
|
18588
18598
|
const parameters = operation.parameters;
|
|
@@ -18635,13 +18645,21 @@ function getReturnInfo(operation) {
|
|
|
18635
18645
|
if (jsonContent === void 0 || jsonContent.schema === void 0) continue;
|
|
18636
18646
|
const schema = jsonContent.schema;
|
|
18637
18647
|
if (isRef(schema)) {
|
|
18638
|
-
return {
|
|
18648
|
+
return {
|
|
18649
|
+
typeName: refToName(schema.$ref),
|
|
18650
|
+
isArray: false,
|
|
18651
|
+
isVoid: false
|
|
18652
|
+
};
|
|
18639
18653
|
}
|
|
18640
18654
|
const s = schema;
|
|
18641
18655
|
if (s.type === "array") {
|
|
18642
18656
|
const items = s.items;
|
|
18643
18657
|
if (items !== void 0 && isRef(items)) {
|
|
18644
|
-
return {
|
|
18658
|
+
return {
|
|
18659
|
+
typeName: refToName(items.$ref),
|
|
18660
|
+
isArray: true,
|
|
18661
|
+
isVoid: false
|
|
18662
|
+
};
|
|
18645
18663
|
}
|
|
18646
18664
|
return { typeName: void 0, isArray: true, isVoid: false };
|
|
18647
18665
|
}
|
|
@@ -18813,7 +18831,18 @@ function deriveOperationName2(method, path) {
|
|
|
18813
18831
|
return prefix + parts.join("");
|
|
18814
18832
|
}
|
|
18815
18833
|
function normalizeParamName2(name) {
|
|
18816
|
-
|
|
18834
|
+
const stripped = name.replace(/\[\]$/, "").replace(/'/g, "");
|
|
18835
|
+
const parts = stripped.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
18836
|
+
if (parts.length === 0) return "_";
|
|
18837
|
+
const camel = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
|
|
18838
|
+
return /^[^a-zA-Z_$]/.test(camel) ? `_${camel}` : camel;
|
|
18839
|
+
}
|
|
18840
|
+
function schemaToTsType(schema) {
|
|
18841
|
+
if (schema === void 0 || isRef2(schema)) return "string";
|
|
18842
|
+
const s = schema;
|
|
18843
|
+
if (s.type === "number" || s.type === "integer") return "number";
|
|
18844
|
+
if (s.type === "boolean") return "boolean";
|
|
18845
|
+
return "string";
|
|
18817
18846
|
}
|
|
18818
18847
|
function getQueryParams2(operation, spec) {
|
|
18819
18848
|
const parameters = operation.parameters;
|
|
@@ -18823,15 +18852,9 @@ function getQueryParams2(operation, spec) {
|
|
|
18823
18852
|
const resolved = resolveParam2(p, spec);
|
|
18824
18853
|
if (resolved === void 0 || resolved.in !== "query") continue;
|
|
18825
18854
|
const schema = resolved.schema;
|
|
18826
|
-
let tsType = "string";
|
|
18827
|
-
if (schema !== void 0 && !isRef2(schema)) {
|
|
18828
|
-
const s = schema;
|
|
18829
|
-
if (s.type === "number" || s.type === "integer") tsType = "number";
|
|
18830
|
-
else if (s.type === "boolean") tsType = "boolean";
|
|
18831
|
-
}
|
|
18832
18855
|
result.push({
|
|
18833
18856
|
name: normalizeParamName2(resolved.name),
|
|
18834
|
-
tsType,
|
|
18857
|
+
tsType: schemaToTsType(schema),
|
|
18835
18858
|
required: resolved.required === true
|
|
18836
18859
|
});
|
|
18837
18860
|
}
|
|
@@ -18852,25 +18875,24 @@ function getBodyInfo2(operation) {
|
|
|
18852
18875
|
}
|
|
18853
18876
|
return { typeName: void 0 };
|
|
18854
18877
|
}
|
|
18878
|
+
function response200IsVoid(resp) {
|
|
18879
|
+
if (isRef2(resp)) return false;
|
|
18880
|
+
const r = resp;
|
|
18881
|
+
const content = r.content;
|
|
18882
|
+
return content === void 0 || Object.keys(content).length === 0;
|
|
18883
|
+
}
|
|
18855
18884
|
function getResponseStatus(operation, httpMethod) {
|
|
18856
18885
|
const responses = operation.responses;
|
|
18857
|
-
if (responses
|
|
18858
|
-
|
|
18859
|
-
if (responses["204"] !== void 0) return { status: 204, isVoid: true };
|
|
18860
|
-
if (responses["200"] !== void 0) {
|
|
18861
|
-
const resp = responses["200"];
|
|
18862
|
-
if (!isRef2(resp)) {
|
|
18863
|
-
const r = resp;
|
|
18864
|
-
const content = r.content;
|
|
18865
|
-
if (content === void 0 || Object.keys(content).length === 0) {
|
|
18866
|
-
return { status: 204, isVoid: true };
|
|
18867
|
-
}
|
|
18868
|
-
}
|
|
18869
|
-
return { status: 200, isVoid: false };
|
|
18870
|
-
}
|
|
18886
|
+
if (responses === void 0) {
|
|
18887
|
+
return httpMethod === "delete" ? { status: 204, isVoid: true } : { status: 200, isVoid: false };
|
|
18871
18888
|
}
|
|
18872
|
-
if (
|
|
18873
|
-
return { status:
|
|
18889
|
+
if (responses["201"] !== void 0) return { status: 201, isVoid: false };
|
|
18890
|
+
if (responses["204"] !== void 0) return { status: 204, isVoid: true };
|
|
18891
|
+
if (responses["200"] !== void 0) {
|
|
18892
|
+
if (response200IsVoid(responses["200"])) return { status: 204, isVoid: true };
|
|
18893
|
+
return { status: 200, isVoid: false };
|
|
18894
|
+
}
|
|
18895
|
+
return httpMethod === "delete" ? { status: 204, isVoid: true } : { status: 200, isVoid: false };
|
|
18874
18896
|
}
|
|
18875
18897
|
function collectOperations2(spec) {
|
|
18876
18898
|
const paths = spec.paths;
|
|
@@ -18919,9 +18941,7 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
18919
18941
|
lines.push(`${indent} const body = await c.req.json${typeAnnotation}()`);
|
|
18920
18942
|
const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
|
|
18921
18943
|
if (schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName)) {
|
|
18922
|
-
lines.push(
|
|
18923
|
-
`${indent} // Validate request body \u2014 returns 422 with Zod issues on failure`
|
|
18924
|
-
);
|
|
18944
|
+
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
18925
18945
|
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(body)`);
|
|
18926
18946
|
lines.push(`${indent} if (!parseResult.success) {`);
|
|
18927
18947
|
lines.push(
|
|
@@ -18954,6 +18974,235 @@ function buildRouteHandler(op, indent, schemaNames) {
|
|
|
18954
18974
|
lines.push(`${indent}})`);
|
|
18955
18975
|
return lines.join("\n");
|
|
18956
18976
|
}
|
|
18977
|
+
function buildExpressRouteHandler(op, indent, schemaNames) {
|
|
18978
|
+
const lines = [];
|
|
18979
|
+
lines.push(
|
|
18980
|
+
`${indent}router.${op.httpMethod}('${op.honoPath}', async (req: Request, res: Response) => {`
|
|
18981
|
+
);
|
|
18982
|
+
if (op.queryParams.length > 0) {
|
|
18983
|
+
const fields = op.queryParams.map((q) => {
|
|
18984
|
+
if (q.tsType === "number") {
|
|
18985
|
+
return ` ${q.name}: Number(req.query['${q.name}'] as string)`;
|
|
18986
|
+
}
|
|
18987
|
+
if (q.tsType === "boolean") {
|
|
18988
|
+
return ` ${q.name}: req.query['${q.name}'] === 'true'`;
|
|
18989
|
+
}
|
|
18990
|
+
return ` ${q.name}: req.query['${q.name}'] as string | undefined`;
|
|
18991
|
+
}).join(",\n");
|
|
18992
|
+
lines.push(`${indent} const params = {`);
|
|
18993
|
+
lines.push(fields);
|
|
18994
|
+
lines.push(`${indent} }`);
|
|
18995
|
+
}
|
|
18996
|
+
let bodyVarName = "body";
|
|
18997
|
+
if (op.bodyInfo !== void 0) {
|
|
18998
|
+
const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
|
|
18999
|
+
const useZod = schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName);
|
|
19000
|
+
if (useZod) {
|
|
19001
|
+
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
19002
|
+
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
|
|
19003
|
+
lines.push(`${indent} if (!parseResult.success) {`);
|
|
19004
|
+
lines.push(
|
|
19005
|
+
`${indent} return void res.status(422).json({ error: 'Invalid request body', issues: parseResult.error.issues })`
|
|
19006
|
+
);
|
|
19007
|
+
lines.push(`${indent} }`);
|
|
19008
|
+
lines.push(`${indent} const validatedBody = parseResult.data`);
|
|
19009
|
+
bodyVarName = "validatedBody";
|
|
19010
|
+
} else {
|
|
19011
|
+
const typeAnnotation = op.bodyInfo.typeName !== void 0 ? ` as ${op.bodyInfo.typeName}` : "";
|
|
19012
|
+
lines.push(`${indent} const body = req.body${typeAnnotation}`);
|
|
19013
|
+
}
|
|
19014
|
+
}
|
|
19015
|
+
const serviceArgs = [];
|
|
19016
|
+
for (const p of op.pathParams) {
|
|
19017
|
+
serviceArgs.push(`req.params['${p}']!`);
|
|
19018
|
+
}
|
|
19019
|
+
if (op.bodyInfo !== void 0) {
|
|
19020
|
+
serviceArgs.push(bodyVarName);
|
|
19021
|
+
}
|
|
19022
|
+
if (op.queryParams.length > 0) {
|
|
19023
|
+
serviceArgs.push("params");
|
|
19024
|
+
}
|
|
19025
|
+
const serviceCall = `service.${op.methodName}(${serviceArgs.join(", ")})`;
|
|
19026
|
+
if (op.responseStatus.isVoid) {
|
|
19027
|
+
lines.push(`${indent} await ${serviceCall}`);
|
|
19028
|
+
lines.push(`${indent} res.status(${op.responseStatus.status}).end()`);
|
|
19029
|
+
} else if (op.responseStatus.status === 201) {
|
|
19030
|
+
lines.push(`${indent} res.status(201).json(await ${serviceCall})`);
|
|
19031
|
+
} else {
|
|
19032
|
+
lines.push(`${indent} res.json(await ${serviceCall})`);
|
|
19033
|
+
}
|
|
19034
|
+
lines.push(`${indent}})`);
|
|
19035
|
+
return lines.join("\n");
|
|
19036
|
+
}
|
|
19037
|
+
function generateExpressRouter(spec, options) {
|
|
19038
|
+
const serviceName = deriveServiceName2(spec);
|
|
19039
|
+
const operations = collectOperations2(spec);
|
|
19040
|
+
const bodyTypes = /* @__PURE__ */ new Set();
|
|
19041
|
+
for (const op of operations) {
|
|
19042
|
+
if (op.bodyInfo?.typeName !== void 0) {
|
|
19043
|
+
bodyTypes.add(op.bodyInfo.typeName);
|
|
19044
|
+
}
|
|
19045
|
+
}
|
|
19046
|
+
const sortedBodyTypes = Array.from(bodyTypes).sort();
|
|
19047
|
+
const usedSchemaNames = /* @__PURE__ */ new Set();
|
|
19048
|
+
if (options?.schemaNames !== void 0) {
|
|
19049
|
+
for (const op of operations) {
|
|
19050
|
+
const typeName = op.bodyInfo?.typeName;
|
|
19051
|
+
if (typeName !== void 0) {
|
|
19052
|
+
const schemaName = `${typeName}Schema`;
|
|
19053
|
+
if (options.schemaNames.has(schemaName)) {
|
|
19054
|
+
usedSchemaNames.add(schemaName);
|
|
19055
|
+
}
|
|
19056
|
+
}
|
|
19057
|
+
}
|
|
19058
|
+
}
|
|
19059
|
+
const lines = [];
|
|
19060
|
+
lines.push("// This file is auto-generated. Do not edit manually.");
|
|
19061
|
+
lines.push(
|
|
19062
|
+
"// Express: apply express.json() middleware before mounting this router so req.body is populated."
|
|
19063
|
+
);
|
|
19064
|
+
lines.push("");
|
|
19065
|
+
lines.push("import { Router } from 'express'");
|
|
19066
|
+
lines.push("import type { Request, Response } from 'express'");
|
|
19067
|
+
if (sortedBodyTypes.length > 0) {
|
|
19068
|
+
lines.push(`import type { ${sortedBodyTypes.join(", ")} } from './models.js'`);
|
|
19069
|
+
}
|
|
19070
|
+
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
19071
|
+
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
|
|
19072
|
+
lines.push(`import { z } from 'zod'`);
|
|
19073
|
+
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
19074
|
+
lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
|
|
19075
|
+
}
|
|
19076
|
+
lines.push("");
|
|
19077
|
+
lines.push(`export function createRouter(service: ${serviceName}): Router {`);
|
|
19078
|
+
lines.push(" const router = Router()");
|
|
19079
|
+
lines.push("");
|
|
19080
|
+
for (const op of operations) {
|
|
19081
|
+
lines.push(buildExpressRouteHandler(op, " ", options?.schemaNames));
|
|
19082
|
+
lines.push("");
|
|
19083
|
+
}
|
|
19084
|
+
lines.push(" return router");
|
|
19085
|
+
lines.push("}");
|
|
19086
|
+
lines.push("");
|
|
19087
|
+
return {
|
|
19088
|
+
filename: "router.ts",
|
|
19089
|
+
content: lines.join("\n")
|
|
19090
|
+
};
|
|
19091
|
+
}
|
|
19092
|
+
function buildFastifyRouteHandler(op, indent, schemaNames) {
|
|
19093
|
+
const lines = [];
|
|
19094
|
+
const genericParts = [];
|
|
19095
|
+
if (op.queryParams.length > 0) {
|
|
19096
|
+
const queryFields = op.queryParams.map((q) => {
|
|
19097
|
+
if (q.tsType === "number") return `${q.name}?: number`;
|
|
19098
|
+
if (q.tsType === "boolean") return `${q.name}?: boolean`;
|
|
19099
|
+
return `${q.name}?: string`;
|
|
19100
|
+
}).join("; ");
|
|
19101
|
+
genericParts.push(`Querystring: { ${queryFields} }`);
|
|
19102
|
+
}
|
|
19103
|
+
if (op.bodyInfo !== void 0 && op.bodyInfo.typeName !== void 0) {
|
|
19104
|
+
genericParts.push(`Body: ${op.bodyInfo.typeName}`);
|
|
19105
|
+
} else if (op.bodyInfo !== void 0) {
|
|
19106
|
+
genericParts.push("Body: unknown");
|
|
19107
|
+
}
|
|
19108
|
+
if (op.pathParams.length > 0) {
|
|
19109
|
+
const paramFields = op.pathParams.map((p) => `${p}: string`).join("; ");
|
|
19110
|
+
genericParts.push(`Params: { ${paramFields} }`);
|
|
19111
|
+
}
|
|
19112
|
+
const generic = genericParts.length > 0 ? `<{ ${genericParts.join("; ")} }>` : "";
|
|
19113
|
+
lines.push(`${indent}app.${op.httpMethod}${generic}('${op.honoPath}', async (req, reply) => {`);
|
|
19114
|
+
if (op.queryParams.length > 0) {
|
|
19115
|
+
const fields = op.queryParams.map((q) => ` ${q.name}: req.query.${q.name}`).join(",\n");
|
|
19116
|
+
lines.push(`${indent} const params = {`);
|
|
19117
|
+
lines.push(fields);
|
|
19118
|
+
lines.push(`${indent} }`);
|
|
19119
|
+
}
|
|
19120
|
+
let bodyVarName = "req.body";
|
|
19121
|
+
if (op.bodyInfo !== void 0) {
|
|
19122
|
+
const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
|
|
19123
|
+
const useZod = schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName);
|
|
19124
|
+
if (useZod) {
|
|
19125
|
+
lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
|
|
19126
|
+
lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
|
|
19127
|
+
lines.push(`${indent} if (!parseResult.success) {`);
|
|
19128
|
+
lines.push(
|
|
19129
|
+
`${indent} return reply.status(422).send({ error: 'Invalid request body', issues: parseResult.error.issues })`
|
|
19130
|
+
);
|
|
19131
|
+
lines.push(`${indent} }`);
|
|
19132
|
+
bodyVarName = "parseResult.data";
|
|
19133
|
+
}
|
|
19134
|
+
}
|
|
19135
|
+
const serviceArgs = [];
|
|
19136
|
+
for (const p of op.pathParams) {
|
|
19137
|
+
serviceArgs.push(`req.params.${p}`);
|
|
19138
|
+
}
|
|
19139
|
+
if (op.bodyInfo !== void 0) {
|
|
19140
|
+
serviceArgs.push(bodyVarName);
|
|
19141
|
+
}
|
|
19142
|
+
if (op.queryParams.length > 0) {
|
|
19143
|
+
serviceArgs.push("params");
|
|
19144
|
+
}
|
|
19145
|
+
const serviceCall = `service.${op.methodName}(${serviceArgs.join(", ")})`;
|
|
19146
|
+
if (op.responseStatus.isVoid) {
|
|
19147
|
+
lines.push(`${indent} await ${serviceCall}`);
|
|
19148
|
+
lines.push(`${indent} reply.status(${op.responseStatus.status}).send()`);
|
|
19149
|
+
} else if (op.responseStatus.status === 201) {
|
|
19150
|
+
lines.push(`${indent} reply.status(201)`);
|
|
19151
|
+
lines.push(`${indent} return ${serviceCall}`);
|
|
19152
|
+
} else {
|
|
19153
|
+
lines.push(`${indent} return ${serviceCall}`);
|
|
19154
|
+
}
|
|
19155
|
+
lines.push(`${indent}})`);
|
|
19156
|
+
return lines.join("\n");
|
|
19157
|
+
}
|
|
19158
|
+
function generateFastifyRouter(spec, options) {
|
|
19159
|
+
const serviceName = deriveServiceName2(spec);
|
|
19160
|
+
const operations = collectOperations2(spec);
|
|
19161
|
+
const bodyTypes = /* @__PURE__ */ new Set();
|
|
19162
|
+
for (const op of operations) {
|
|
19163
|
+
if (op.bodyInfo?.typeName !== void 0) {
|
|
19164
|
+
bodyTypes.add(op.bodyInfo.typeName);
|
|
19165
|
+
}
|
|
19166
|
+
}
|
|
19167
|
+
const sortedBodyTypes = Array.from(bodyTypes).sort();
|
|
19168
|
+
const usedSchemaNames = /* @__PURE__ */ new Set();
|
|
19169
|
+
if (options?.schemaNames !== void 0) {
|
|
19170
|
+
for (const op of operations) {
|
|
19171
|
+
const typeName = op.bodyInfo?.typeName;
|
|
19172
|
+
if (typeName !== void 0) {
|
|
19173
|
+
const schemaName = `${typeName}Schema`;
|
|
19174
|
+
if (options.schemaNames.has(schemaName)) {
|
|
19175
|
+
usedSchemaNames.add(schemaName);
|
|
19176
|
+
}
|
|
19177
|
+
}
|
|
19178
|
+
}
|
|
19179
|
+
}
|
|
19180
|
+
const lines = [];
|
|
19181
|
+
lines.push("// This file is auto-generated. Do not edit manually.");
|
|
19182
|
+
lines.push("");
|
|
19183
|
+
lines.push("import type { FastifyInstance } from 'fastify'");
|
|
19184
|
+
if (sortedBodyTypes.length > 0) {
|
|
19185
|
+
lines.push(`import type { ${sortedBodyTypes.join(", ")} } from './models.js'`);
|
|
19186
|
+
}
|
|
19187
|
+
lines.push(`import type { ${serviceName} } from './service.js'`);
|
|
19188
|
+
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
|
|
19189
|
+
lines.push(`import { z } from 'zod'`);
|
|
19190
|
+
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
19191
|
+
lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
|
|
19192
|
+
}
|
|
19193
|
+
lines.push("");
|
|
19194
|
+
lines.push(`export function createRouter(app: FastifyInstance, service: ${serviceName}): void {`);
|
|
19195
|
+
for (const op of operations) {
|
|
19196
|
+
lines.push("");
|
|
19197
|
+
lines.push(buildFastifyRouteHandler(op, " ", options?.schemaNames));
|
|
19198
|
+
}
|
|
19199
|
+
lines.push("}");
|
|
19200
|
+
lines.push("");
|
|
19201
|
+
return {
|
|
19202
|
+
filename: "router.ts",
|
|
19203
|
+
content: lines.join("\n")
|
|
19204
|
+
};
|
|
19205
|
+
}
|
|
18957
19206
|
function generateRouter(spec, options) {
|
|
18958
19207
|
const serviceName = deriveServiceName2(spec);
|
|
18959
19208
|
const operations = collectOperations2(spec);
|
|
@@ -18987,9 +19236,7 @@ function generateRouter(spec, options) {
|
|
|
18987
19236
|
if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
|
|
18988
19237
|
lines.push(`import { z } from 'zod'`);
|
|
18989
19238
|
const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
|
|
18990
|
-
lines.push(
|
|
18991
|
-
`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`
|
|
18992
|
-
);
|
|
19239
|
+
lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
|
|
18993
19240
|
}
|
|
18994
19241
|
lines.push("");
|
|
18995
19242
|
lines.push(`export function createRouter(service: ${serviceName}): Hono {`);
|
|
@@ -19026,6 +19273,10 @@ async function generate2(cwd2, configPath) {
|
|
|
19026
19273
|
generatedFiles.push(generateService(spec));
|
|
19027
19274
|
if (framework === "hono") {
|
|
19028
19275
|
generatedFiles.push(generateRouter(spec));
|
|
19276
|
+
} else if (framework === "express") {
|
|
19277
|
+
generatedFiles.push(generateExpressRouter(spec));
|
|
19278
|
+
} else if (framework === "fastify") {
|
|
19279
|
+
generatedFiles.push(generateFastifyRouter(spec));
|
|
19029
19280
|
}
|
|
19030
19281
|
console.log(`Writing output to: ${outputDir}`);
|
|
19031
19282
|
await (0, import_promises2.mkdir)(outputDir, { recursive: true });
|
|
@@ -19034,7 +19285,7 @@ async function generate2(cwd2, configPath) {
|
|
|
19034
19285
|
await (0, import_promises2.writeFile)(filePath, await formatTs(file.content, filePath), "utf-8");
|
|
19035
19286
|
console.log(` \u2713 ${file.filename}`);
|
|
19036
19287
|
}
|
|
19037
|
-
if (framework === "hono" && config.input_schema !== void 0) {
|
|
19288
|
+
if ((framework === "hono" || framework === "express" || framework === "fastify") && config.input_schema !== void 0) {
|
|
19038
19289
|
const schemaPath = (0, import_node_path2.resolve)(cwd2, config.input_schema);
|
|
19039
19290
|
let schemaContent;
|
|
19040
19291
|
try {
|
|
@@ -19052,10 +19303,15 @@ async function generate2(cwd2, configPath) {
|
|
|
19052
19303
|
const relPath = (0, import_node_path2.relative)(outputDir, schemaPath).replace(/\\/g, "/");
|
|
19053
19304
|
const schemaImportPath = relPath.startsWith(".") ? relPath : `./${relPath}`;
|
|
19054
19305
|
const schemaImportPathJs = schemaImportPath.replace(/\.ts$/, ".js");
|
|
19055
|
-
const
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19306
|
+
const routerOptions = { schemaNames: exportedSchemas, schemaImportPath: schemaImportPathJs };
|
|
19307
|
+
let routerFile;
|
|
19308
|
+
if (framework === "hono") {
|
|
19309
|
+
routerFile = generateRouter(spec, routerOptions);
|
|
19310
|
+
} else if (framework === "express") {
|
|
19311
|
+
routerFile = generateExpressRouter(spec, routerOptions);
|
|
19312
|
+
} else {
|
|
19313
|
+
routerFile = generateFastifyRouter(spec, routerOptions);
|
|
19314
|
+
}
|
|
19059
19315
|
const routerPath = (0, import_node_path2.join)(outputDir, routerFile.filename);
|
|
19060
19316
|
await (0, import_promises2.writeFile)(routerPath, await formatTs(routerFile.content, routerPath), "utf-8");
|
|
19061
19317
|
console.log(` \u2713 router.ts (with Zod validation for ${exportedSchemas.size} schema(s))`);
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface ServerConfig {
|
|
|
4
4
|
/** Directory to write generated files */
|
|
5
5
|
output: string;
|
|
6
6
|
/** Framework to generate a router for. Default: 'none' */
|
|
7
|
-
framework?: 'hono' | 'none';
|
|
7
|
+
framework?: 'hono' | 'express' | 'fastify' | 'none';
|
|
8
8
|
/** Path to user-owned Zod schema file (same file as openapi-gen's input_schema). Optional. */
|
|
9
9
|
input_schema?: string;
|
|
10
10
|
}
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAA;IACnD,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAgCD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAW/D;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAW7D;AAGD,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA4DxF"}
|