next-openapi-gen 0.6.1 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/dist/lib/openapi-generator.js +85 -1
- package/dist/lib/route-processor.js +93 -4
- package/dist/lib/schema-processor.js +70 -6
- package/dist/lib/utils.js +53 -9
- package/dist/lib/zod-converter.js +16 -0
- package/dist/openapi-template.js +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,9 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
|
|
|
75
75
|
| `outputFile` | Path to the OpenAPI output file |
|
|
76
76
|
| `docsUrl` | API documentation URL (for Swagger UI) |
|
|
77
77
|
| `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
|
|
78
|
+
| `defaultResponseSet` | Default error response set for all endpoints |
|
|
79
|
+
| `responseSets` | Named sets of error response codes |
|
|
80
|
+
| `errorConfig` | Error schema configuration |
|
|
78
81
|
|
|
79
82
|
## Documenting Your API
|
|
80
83
|
|
|
@@ -154,6 +157,9 @@ export async function GET(
|
|
|
154
157
|
| `@bodyDescription` | Request body description |
|
|
155
158
|
| `@response` | Response type/schema |
|
|
156
159
|
| `@responseDescription` | Response description |
|
|
160
|
+
| `@responseSet` | Override default response set (`public`, `auth`, `none`) |
|
|
161
|
+
| `@add` | Add custom response codes (`409:ConflictResponse`, `429`) |
|
|
162
|
+
| `@contentType` | Request body content type (`application/json`, `multipart/form-data`) |
|
|
157
163
|
| `@auth` | Authorization type (`bearer`, `basic`, `apikey`) |
|
|
158
164
|
| `@tag` | Custom tag |
|
|
159
165
|
| `@deprecated` | Marks the route as deprecated |
|
|
@@ -344,6 +350,117 @@ export async function GET() {
|
|
|
344
350
|
}
|
|
345
351
|
```
|
|
346
352
|
|
|
353
|
+
### File Uploads / Multipart Form Data
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// src/app/api/upload/route.ts
|
|
357
|
+
|
|
358
|
+
// TypeScript
|
|
359
|
+
type FileUploadFormData = {
|
|
360
|
+
file: File;
|
|
361
|
+
description?: string;
|
|
362
|
+
category: string;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Or Zod
|
|
366
|
+
const FileUploadSchema = z.object({
|
|
367
|
+
file: z.custom<File>().describe("Image file (PNG/JPG)"),
|
|
368
|
+
description: z.string().optional().describe("File description"),
|
|
369
|
+
category: z.string().describe("File category"),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @body FileUploadSchema
|
|
374
|
+
* @contentType multipart/form-data
|
|
375
|
+
*/
|
|
376
|
+
export async function POST() {
|
|
377
|
+
// ...
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Response Management
|
|
382
|
+
|
|
383
|
+
### Zero Config + Response Sets
|
|
384
|
+
|
|
385
|
+
Configure reusable error sets in `next.openapi.json`:
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"defaultResponseSet": "common",
|
|
390
|
+
"responseSets": {
|
|
391
|
+
"common": ["400", "401", "500"],
|
|
392
|
+
"public": ["400", "500"],
|
|
393
|
+
"auth": ["400", "401", "403", "500"]
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Usage Examples
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
/**
|
|
402
|
+
* Auto-default responses
|
|
403
|
+
* @response UserResponse
|
|
404
|
+
* @openapi
|
|
405
|
+
*/
|
|
406
|
+
export async function GET() {}
|
|
407
|
+
// Generates: 200:UserResponse + common errors (400, 401, 500)
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Override response set
|
|
411
|
+
* @response ProductResponse
|
|
412
|
+
* @responseSet public
|
|
413
|
+
* @openapi
|
|
414
|
+
*/
|
|
415
|
+
export async function GET() {}
|
|
416
|
+
// Generates: 200:ProductResponse + public errors (400, 500)
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Add custom responses
|
|
420
|
+
* @response 201:UserResponse
|
|
421
|
+
* @add 409:ConflictResponse
|
|
422
|
+
* @openapi
|
|
423
|
+
*/
|
|
424
|
+
export async function POST() {}
|
|
425
|
+
// Generates: 201:UserResponse + common errors + 409:ConflictResponse
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Combine multiple sets
|
|
429
|
+
* @response UserResponse
|
|
430
|
+
* @responseSet auth,crud
|
|
431
|
+
* @add 429:RateLimitResponse
|
|
432
|
+
* @openapi
|
|
433
|
+
*/
|
|
434
|
+
export async function PUT() {}
|
|
435
|
+
// Combines: auth + crud errors + custom 429
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Error Schema Configuration
|
|
439
|
+
|
|
440
|
+
#### Define consistent error schemas using templates:
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"errorConfig": {
|
|
445
|
+
"template": {
|
|
446
|
+
"type": "object",
|
|
447
|
+
"properties": {
|
|
448
|
+
"error": {
|
|
449
|
+
"type": "string",
|
|
450
|
+
"example": "{{ERROR_MESSAGE}}"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"codes": {
|
|
455
|
+
"invalid_request": {
|
|
456
|
+
"description": "Invalid request",
|
|
457
|
+
"variables": { "ERROR_MESSAGE": "Validation failed" }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
347
464
|
## Advanced Usage
|
|
348
465
|
|
|
349
466
|
### Automatic Path Parameter Detection
|
|
@@ -14,7 +14,7 @@ export class OpenApiGenerator {
|
|
|
14
14
|
}
|
|
15
15
|
getConfig() {
|
|
16
16
|
// @ts-ignore
|
|
17
|
-
const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript" } = this.template;
|
|
17
|
+
const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig } = this.template;
|
|
18
18
|
return {
|
|
19
19
|
apiDir,
|
|
20
20
|
schemaDir,
|
|
@@ -23,6 +23,9 @@ export class OpenApiGenerator {
|
|
|
23
23
|
outputFile,
|
|
24
24
|
includeOpenApiRoutes,
|
|
25
25
|
schemaType,
|
|
26
|
+
defaultResponseSet,
|
|
27
|
+
responseSets,
|
|
28
|
+
errorConfig,
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
generate() {
|
|
@@ -57,6 +60,21 @@ export class OpenApiGenerator {
|
|
|
57
60
|
if (!this.template.components.schemas) {
|
|
58
61
|
this.template.components.schemas = {};
|
|
59
62
|
}
|
|
63
|
+
// Generate error responses using errorConfig or manual definitions
|
|
64
|
+
if (!this.template.components.responses) {
|
|
65
|
+
this.template.components.responses = {};
|
|
66
|
+
}
|
|
67
|
+
const errorConfig = this.config.errorConfig;
|
|
68
|
+
if (errorConfig) {
|
|
69
|
+
this.generateErrorResponsesFromConfig(errorConfig);
|
|
70
|
+
}
|
|
71
|
+
else if (this.config.errorDefinitions) {
|
|
72
|
+
// Use manual definitions (existing logic - if exists)
|
|
73
|
+
Object.entries(this.config.errorDefinitions).forEach(([code, errorDef]) => {
|
|
74
|
+
this.template.components.responses[code] =
|
|
75
|
+
this.createErrorResponseComponent(code, errorDef);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
60
78
|
// Get defined schemas from the processor
|
|
61
79
|
const definedSchemas = this.routeProcessor
|
|
62
80
|
.getSchemaProcessor()
|
|
@@ -70,4 +88,70 @@ export class OpenApiGenerator {
|
|
|
70
88
|
const openapiSpec = cleanSpec(this.template);
|
|
71
89
|
return openapiSpec;
|
|
72
90
|
}
|
|
91
|
+
generateErrorResponsesFromConfig(errorConfig) {
|
|
92
|
+
const { template, codes, variables: globalVars = {} } = errorConfig;
|
|
93
|
+
Object.entries(codes).forEach(([errorCode, config]) => {
|
|
94
|
+
const httpStatus = (config.httpStatus || this.guessHttpStatus(errorCode)).toString();
|
|
95
|
+
// Merge variables: global + per-code + built-in
|
|
96
|
+
const allVariables = {
|
|
97
|
+
...globalVars,
|
|
98
|
+
...config.variables,
|
|
99
|
+
ERROR_CODE: errorCode,
|
|
100
|
+
DESCRIPTION: config.description,
|
|
101
|
+
HTTP_STATUS: httpStatus,
|
|
102
|
+
};
|
|
103
|
+
const processedSchema = this.processTemplate(template, allVariables);
|
|
104
|
+
this.template.components.responses[httpStatus] = {
|
|
105
|
+
description: config.description,
|
|
106
|
+
content: {
|
|
107
|
+
"application/json": {
|
|
108
|
+
schema: processedSchema,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
processTemplate(template, variables) {
|
|
115
|
+
const jsonStr = JSON.stringify(template);
|
|
116
|
+
let result = jsonStr;
|
|
117
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
118
|
+
result = result.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
119
|
+
});
|
|
120
|
+
return JSON.parse(result);
|
|
121
|
+
}
|
|
122
|
+
guessHttpStatus(errorCode) {
|
|
123
|
+
const statusMap = {
|
|
124
|
+
bad: 400,
|
|
125
|
+
invalid: 400,
|
|
126
|
+
validation: 422,
|
|
127
|
+
unauthorized: 401,
|
|
128
|
+
auth: 401,
|
|
129
|
+
forbidden: 403,
|
|
130
|
+
permission: 403,
|
|
131
|
+
not_found: 404,
|
|
132
|
+
missing: 404,
|
|
133
|
+
conflict: 409,
|
|
134
|
+
duplicate: 409,
|
|
135
|
+
rate_limit: 429,
|
|
136
|
+
too_many: 429,
|
|
137
|
+
server: 500,
|
|
138
|
+
internal: 500,
|
|
139
|
+
};
|
|
140
|
+
for (const [key, status] of Object.entries(statusMap)) {
|
|
141
|
+
if (errorCode.toLowerCase().includes(key)) {
|
|
142
|
+
return status;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return 500;
|
|
146
|
+
}
|
|
147
|
+
createErrorResponseComponent(code, errorDef) {
|
|
148
|
+
return {
|
|
149
|
+
description: errorDef.description,
|
|
150
|
+
content: {
|
|
151
|
+
"application/json": {
|
|
152
|
+
schema: errorDef.schema,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
73
157
|
}
|
|
@@ -18,6 +18,91 @@ export class RouteProcessor {
|
|
|
18
18
|
this.config = config;
|
|
19
19
|
this.schemaProcessor = new SchemaProcessor(config.schemaDir, config.schemaType);
|
|
20
20
|
}
|
|
21
|
+
buildResponsesFromConfig(dataTypes, method) {
|
|
22
|
+
const responses = {};
|
|
23
|
+
// 1. Add success response
|
|
24
|
+
const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
|
|
25
|
+
if (dataTypes.responseType) {
|
|
26
|
+
const responseSchema = this.schemaProcessor.getSchemaContent({
|
|
27
|
+
responseType: dataTypes.responseType,
|
|
28
|
+
}).responses;
|
|
29
|
+
responses[successCode] = {
|
|
30
|
+
description: dataTypes.responseDescription || "Successful response",
|
|
31
|
+
content: {
|
|
32
|
+
"application/json": {
|
|
33
|
+
schema: responseSchema,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// 2. Add responses from ResponseSet
|
|
39
|
+
const responseSetName = dataTypes.responseSet || this.config.defaultResponseSet;
|
|
40
|
+
if (responseSetName && responseSetName !== "none") {
|
|
41
|
+
const responseSets = this.config.responseSets || {};
|
|
42
|
+
const setNames = responseSetName.split(",").map((s) => s.trim());
|
|
43
|
+
setNames.forEach((setName) => {
|
|
44
|
+
const responseSet = responseSets[setName];
|
|
45
|
+
if (responseSet) {
|
|
46
|
+
responseSet.forEach((errorCode) => {
|
|
47
|
+
// Use $ref for components/responses
|
|
48
|
+
responses[errorCode] = {
|
|
49
|
+
$ref: `#/components/responses/${errorCode}`,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// 3. Add custom responses (@add)
|
|
56
|
+
if (dataTypes.addResponses) {
|
|
57
|
+
const customResponses = dataTypes.addResponses
|
|
58
|
+
.split(",")
|
|
59
|
+
.map((s) => s.trim());
|
|
60
|
+
customResponses.forEach((responseRef) => {
|
|
61
|
+
const [code, ref] = responseRef.split(":");
|
|
62
|
+
if (ref) {
|
|
63
|
+
// Custom schema: "409:ConflictResponse"
|
|
64
|
+
responses[code] = {
|
|
65
|
+
description: this.getDefaultErrorDescription(code) || `HTTP ${code} response`,
|
|
66
|
+
content: {
|
|
67
|
+
"application/json": {
|
|
68
|
+
schema: { $ref: `#/components/schemas/${ref}` },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Only code: "409" - use $ref fro components/responses
|
|
75
|
+
responses[code] = {
|
|
76
|
+
$ref: `#/components/responses/${code}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return responses;
|
|
82
|
+
}
|
|
83
|
+
getDefaultSuccessCode(method) {
|
|
84
|
+
switch (method.toUpperCase()) {
|
|
85
|
+
case "POST":
|
|
86
|
+
return "201";
|
|
87
|
+
case "DELETE":
|
|
88
|
+
return "204";
|
|
89
|
+
default:
|
|
90
|
+
return "200";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getDefaultErrorDescription(code) {
|
|
94
|
+
const defaults = {
|
|
95
|
+
400: "Bad Request",
|
|
96
|
+
401: "Unauthorized",
|
|
97
|
+
403: "Forbidden",
|
|
98
|
+
404: "Not Found",
|
|
99
|
+
409: "Conflict",
|
|
100
|
+
422: "Unprocessable Entity",
|
|
101
|
+
429: "Too Many Requests",
|
|
102
|
+
500: "Internal Server Error",
|
|
103
|
+
};
|
|
104
|
+
return defaults[code] || `HTTP ${code}`;
|
|
105
|
+
}
|
|
21
106
|
/**
|
|
22
107
|
* Get the SchemaProcessor instance
|
|
23
108
|
*/
|
|
@@ -160,12 +245,16 @@ export class RouteProcessor {
|
|
|
160
245
|
}
|
|
161
246
|
// Add request body
|
|
162
247
|
if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
|
|
163
|
-
definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription);
|
|
248
|
+
definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
|
|
164
249
|
}
|
|
165
250
|
// Add responses
|
|
166
|
-
definition.responses =
|
|
167
|
-
|
|
168
|
-
|
|
251
|
+
definition.responses = this.buildResponsesFromConfig(dataTypes, method);
|
|
252
|
+
// If there are no responses from config, use the old logic
|
|
253
|
+
if (Object.keys(definition.responses).length === 0) {
|
|
254
|
+
definition.responses = responses
|
|
255
|
+
? this.schemaProcessor.createResponseSchema(responses, responseDescription)
|
|
256
|
+
: {};
|
|
257
|
+
}
|
|
169
258
|
this.swaggerPaths[routePath][method] = definition;
|
|
170
259
|
}
|
|
171
260
|
getRoutePath(filePath) {
|
|
@@ -501,6 +501,64 @@ export class SchemaProcessor {
|
|
|
501
501
|
return "example";
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
+
detectContentType(bodyType, explicitContentType) {
|
|
505
|
+
if (explicitContentType) {
|
|
506
|
+
return explicitContentType;
|
|
507
|
+
}
|
|
508
|
+
// Automatic detection based on type name
|
|
509
|
+
if (bodyType &&
|
|
510
|
+
(bodyType.toLowerCase().includes("formdata") ||
|
|
511
|
+
bodyType.toLowerCase().includes("fileupload") ||
|
|
512
|
+
bodyType.toLowerCase().includes("multipart"))) {
|
|
513
|
+
return "multipart/form-data";
|
|
514
|
+
}
|
|
515
|
+
return "application/json";
|
|
516
|
+
}
|
|
517
|
+
createMultipleResponsesSchema(responses, defaultDescription) {
|
|
518
|
+
const result = {};
|
|
519
|
+
Object.entries(responses).forEach(([code, response]) => {
|
|
520
|
+
if (typeof response === "string") {
|
|
521
|
+
// Reference do components/responses
|
|
522
|
+
result[code] = { $ref: `#/components/responses/${response}` };
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
result[code] = {
|
|
526
|
+
description: response.description || defaultDescription || "Response",
|
|
527
|
+
content: {
|
|
528
|
+
"application/json": {
|
|
529
|
+
schema: response.schema || response,
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
createFormDataSchema(body) {
|
|
538
|
+
if (!body.properties) {
|
|
539
|
+
return body;
|
|
540
|
+
}
|
|
541
|
+
const formDataProperties = {};
|
|
542
|
+
Object.entries(body.properties).forEach(([key, value]) => {
|
|
543
|
+
// Convert File types to binary format
|
|
544
|
+
if (value.type === "object" &&
|
|
545
|
+
(key.toLowerCase().includes("file") ||
|
|
546
|
+
value.description?.toLowerCase().includes("file"))) {
|
|
547
|
+
formDataProperties[key] = {
|
|
548
|
+
type: "string",
|
|
549
|
+
format: "binary",
|
|
550
|
+
description: value.description,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
formDataProperties[key] = value;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return {
|
|
558
|
+
...body,
|
|
559
|
+
properties: formDataProperties,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
504
562
|
/**
|
|
505
563
|
* Create a default schema for path parameters when no schema is defined
|
|
506
564
|
*/
|
|
@@ -558,18 +616,24 @@ export class SchemaProcessor {
|
|
|
558
616
|
}
|
|
559
617
|
return queryParams;
|
|
560
618
|
}
|
|
561
|
-
createRequestBodySchema(body, description) {
|
|
562
|
-
const
|
|
619
|
+
createRequestBodySchema(body, description, contentType) {
|
|
620
|
+
const detectedContentType = this.detectContentType(body?.type || "", contentType);
|
|
621
|
+
let schema = body;
|
|
622
|
+
// If it is multipart/form-data, convert schema
|
|
623
|
+
if (detectedContentType === "multipart/form-data") {
|
|
624
|
+
schema = this.createFormDataSchema(body);
|
|
625
|
+
}
|
|
626
|
+
const requestBody = {
|
|
563
627
|
content: {
|
|
564
|
-
|
|
565
|
-
schema:
|
|
628
|
+
[detectedContentType]: {
|
|
629
|
+
schema: schema,
|
|
566
630
|
},
|
|
567
631
|
},
|
|
568
632
|
};
|
|
569
633
|
if (description) {
|
|
570
|
-
|
|
634
|
+
requestBody.description = description;
|
|
571
635
|
}
|
|
572
|
-
return
|
|
636
|
+
return requestBody;
|
|
573
637
|
}
|
|
574
638
|
createResponseSchema(responses, description) {
|
|
575
639
|
return {
|
package/dist/lib/utils.js
CHANGED
|
@@ -22,12 +22,16 @@ export function extractJSDocComments(path) {
|
|
|
22
22
|
let paramsType = "";
|
|
23
23
|
let pathParamsType = "";
|
|
24
24
|
let bodyType = "";
|
|
25
|
-
let responseType = "";
|
|
26
25
|
let auth = "";
|
|
27
26
|
let isOpenApi = false;
|
|
28
27
|
let deprecated = false;
|
|
29
28
|
let bodyDescription = "";
|
|
29
|
+
let contentType = "";
|
|
30
|
+
let responseType = "";
|
|
30
31
|
let responseDescription = "";
|
|
32
|
+
let responseSet = "";
|
|
33
|
+
let addResponses = "";
|
|
34
|
+
let successCode = "";
|
|
31
35
|
if (comments) {
|
|
32
36
|
comments.forEach((comment) => {
|
|
33
37
|
const commentValue = cleanComment(comment.value);
|
|
@@ -42,13 +46,6 @@ export function extractJSDocComments(path) {
|
|
|
42
46
|
bodyDescription = match[1].trim();
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
if (commentValue.includes("@responseDescription")) {
|
|
46
|
-
const regex = /@responseDescription\s*(.*)/;
|
|
47
|
-
const match = commentValue.match(regex);
|
|
48
|
-
if (match && match[1]) {
|
|
49
|
-
responseDescription = match[1].trim();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
49
|
if (!summary) {
|
|
53
50
|
summary = commentValue.split("\n")[0];
|
|
54
51
|
}
|
|
@@ -90,6 +87,45 @@ export function extractJSDocComments(path) {
|
|
|
90
87
|
if (commentValue.includes("@response")) {
|
|
91
88
|
responseType = extractTypeFromComment(commentValue, "@response");
|
|
92
89
|
}
|
|
90
|
+
if (commentValue.includes("@contentType")) {
|
|
91
|
+
const regex = /@contentType\s*(.*)/;
|
|
92
|
+
const match = commentValue.match(regex);
|
|
93
|
+
if (match && match[1]) {
|
|
94
|
+
contentType = match[1].trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (commentValue.includes("@responseDescription")) {
|
|
98
|
+
const regex = /@responseDescription\s*(.*)/;
|
|
99
|
+
const match = commentValue.match(regex);
|
|
100
|
+
if (match && match[1]) {
|
|
101
|
+
responseDescription = match[1].trim();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (commentValue.includes("@responseSet")) {
|
|
105
|
+
const regex = /@responseSet\s*(.*)/;
|
|
106
|
+
const match = commentValue.match(regex);
|
|
107
|
+
if (match && match[1]) {
|
|
108
|
+
responseSet = match[1].trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (commentValue.includes("@add")) {
|
|
112
|
+
const regex = /@add\s*(.*)/;
|
|
113
|
+
const match = commentValue.match(regex);
|
|
114
|
+
if (match && match[1]) {
|
|
115
|
+
addResponses = match[1].trim();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (commentValue.includes("@response")) {
|
|
119
|
+
const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?(\w+)(?:\s+(.*))?/);
|
|
120
|
+
if (responseMatch) {
|
|
121
|
+
const [, code, type] = responseMatch;
|
|
122
|
+
successCode = code || "";
|
|
123
|
+
responseType = type;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
responseType = extractTypeFromComment(commentValue, "@response");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
93
129
|
});
|
|
94
130
|
}
|
|
95
131
|
return {
|
|
@@ -100,11 +136,15 @@ export function extractJSDocComments(path) {
|
|
|
100
136
|
paramsType,
|
|
101
137
|
pathParamsType,
|
|
102
138
|
bodyType,
|
|
103
|
-
responseType,
|
|
104
139
|
isOpenApi,
|
|
105
140
|
deprecated,
|
|
106
141
|
bodyDescription,
|
|
142
|
+
contentType,
|
|
143
|
+
responseType,
|
|
107
144
|
responseDescription,
|
|
145
|
+
responseSet,
|
|
146
|
+
addResponses,
|
|
147
|
+
successCode,
|
|
108
148
|
};
|
|
109
149
|
}
|
|
110
150
|
export function extractTypeFromComment(commentValue, tag) {
|
|
@@ -121,6 +161,10 @@ export function cleanSpec(spec) {
|
|
|
121
161
|
"ui",
|
|
122
162
|
"outputFile",
|
|
123
163
|
"includeOpenApiRoutes",
|
|
164
|
+
"schemaType",
|
|
165
|
+
"defaultResponseSet",
|
|
166
|
+
"responseSets",
|
|
167
|
+
"errorConfig",
|
|
124
168
|
];
|
|
125
169
|
const newSpec = { ...spec };
|
|
126
170
|
propsToRemove.forEach((key) => delete newSpec[key]);
|
|
@@ -846,6 +846,22 @@ export class ZodSchemaConverter {
|
|
|
846
846
|
!t.isIdentifier(node.callee.property)) {
|
|
847
847
|
return { type: "string" };
|
|
848
848
|
}
|
|
849
|
+
if (t.isMemberExpression(node.callee) &&
|
|
850
|
+
t.isIdentifier(node.callee.property)) {
|
|
851
|
+
const zodType = node.callee.property.name;
|
|
852
|
+
// Custom() support for FormData
|
|
853
|
+
if (zodType === "custom" && node.arguments.length > 0) {
|
|
854
|
+
// Check if it is FormData
|
|
855
|
+
if (t.isArrowFunctionExpression(node.arguments[0])) {
|
|
856
|
+
// Assume custom FormData validation
|
|
857
|
+
return {
|
|
858
|
+
type: "object",
|
|
859
|
+
additionalProperties: true,
|
|
860
|
+
description: "Form data object",
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
849
865
|
const zodType = node.callee.property.name;
|
|
850
866
|
let schema = {};
|
|
851
867
|
// Basic type mapping
|
package/dist/openapi-template.js
CHANGED
|
@@ -20,6 +20,49 @@ export default {
|
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
+
defaultResponseSet: "common",
|
|
24
|
+
responseSets: {
|
|
25
|
+
common: ["400", "500"],
|
|
26
|
+
auth: ["401"],
|
|
27
|
+
},
|
|
28
|
+
errorConfig: {
|
|
29
|
+
template: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
success: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
example: false,
|
|
35
|
+
},
|
|
36
|
+
error: {
|
|
37
|
+
type: "string",
|
|
38
|
+
example: "{{ERROR_MESSAGE}}",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
codes: {
|
|
43
|
+
invalid: {
|
|
44
|
+
description: "Bad request",
|
|
45
|
+
httpStatus: 400,
|
|
46
|
+
variables: {
|
|
47
|
+
ERROR_MESSAGE: "Validation error",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
auth: {
|
|
51
|
+
description: "Unauthorized",
|
|
52
|
+
httpStatus: 401,
|
|
53
|
+
variables: {
|
|
54
|
+
ERROR_MESSAGE: "Unathorized",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
server_error: {
|
|
58
|
+
description: "Internal server error",
|
|
59
|
+
httpStatus: 500,
|
|
60
|
+
variables: {
|
|
61
|
+
ERROR_MESSAGE: "Something went wrong",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
23
66
|
apiDir: "./src/app/api",
|
|
24
67
|
schemaDir: "./src",
|
|
25
68
|
schemaType: "typescript",
|
package/package.json
CHANGED