enlace-openapi 0.0.1-beta.2 → 0.0.1-beta.3
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/LICENSE +21 -0
- package/README.md +20 -8
- package/dist/cli.js +79 -10
- package/dist/cli.mjs +79 -10
- package/dist/index.d.mts +15 -9
- package/dist/index.d.ts +15 -9
- package/dist/index.js +79 -10
- package/dist/index.mjs +79 -10
- package/package.json +8 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Enlace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -186,18 +186,30 @@ Parameter names are auto-generated from the parent segment (e.g., `users` → `u
|
|
|
186
186
|
|
|
187
187
|
## Programmatic API
|
|
188
188
|
|
|
189
|
+
### Next.js + Swagger UI Example
|
|
190
|
+
|
|
189
191
|
```typescript
|
|
192
|
+
import SwaggerUI from "swagger-ui-react";
|
|
193
|
+
import "swagger-ui-react/swagger-ui.css";
|
|
190
194
|
import { parseSchema, generateOpenAPISpec } from "enlace-openapi";
|
|
191
195
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
const spec = (() => {
|
|
197
|
+
const { endpoints, schemas } = parseSchema(
|
|
198
|
+
"./APISchema.ts",
|
|
199
|
+
"ApiSchema"
|
|
200
|
+
);
|
|
201
|
+
return generateOpenAPISpec(endpoints, schemas, {
|
|
202
|
+
title: "My API",
|
|
203
|
+
version: "1.0.0",
|
|
204
|
+
baseUrl: "https://api.example.com",
|
|
205
|
+
});
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
const DocsPage = () => {
|
|
209
|
+
return <SwaggerUI spec={spec} />;
|
|
210
|
+
};
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
export default DocsPage;
|
|
201
213
|
```
|
|
202
214
|
|
|
203
215
|
## Viewing the OpenAPI Spec
|
package/dist/cli.js
CHANGED
|
@@ -299,16 +299,20 @@ function isEndpointStructure(type) {
|
|
|
299
299
|
propNames.delete("data");
|
|
300
300
|
propNames.delete("body");
|
|
301
301
|
propNames.delete("error");
|
|
302
|
+
propNames.delete("query");
|
|
303
|
+
propNames.delete("formData");
|
|
302
304
|
const remainingProps = [...propNames].filter(
|
|
303
305
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
304
306
|
);
|
|
305
307
|
return remainingProps.length === 0;
|
|
306
308
|
}
|
|
307
|
-
function parseEndpointType(type,
|
|
309
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
308
310
|
const { checker } = ctx;
|
|
309
311
|
let dataType;
|
|
310
312
|
let bodyType;
|
|
311
313
|
let errorType;
|
|
314
|
+
let queryType;
|
|
315
|
+
let formDataType;
|
|
312
316
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
313
317
|
for (const t of typesToCheck) {
|
|
314
318
|
const props = t.getProperties();
|
|
@@ -320,32 +324,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
320
324
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
321
325
|
} else if (name === "error") {
|
|
322
326
|
errorType = checker.getTypeOfSymbol(prop);
|
|
327
|
+
} else if (name === "query") {
|
|
328
|
+
queryType = checker.getTypeOfSymbol(prop);
|
|
329
|
+
} else if (name === "formData") {
|
|
330
|
+
formDataType = checker.getTypeOfSymbol(prop);
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
333
|
}
|
|
326
334
|
const endpoint = {
|
|
327
|
-
path:
|
|
335
|
+
path: pathStr,
|
|
328
336
|
method,
|
|
329
337
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
330
338
|
pathParams
|
|
331
339
|
};
|
|
332
|
-
if (
|
|
340
|
+
if (formDataType && !(formDataType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
341
|
+
endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
|
|
342
|
+
endpoint.requestBodyContentType = "multipart/form-data";
|
|
343
|
+
} else if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
333
344
|
endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
|
|
345
|
+
endpoint.requestBodyContentType = "application/json";
|
|
346
|
+
}
|
|
347
|
+
if (queryType && !(queryType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
348
|
+
endpoint.queryParams = queryTypeToParams(queryType, ctx);
|
|
334
349
|
}
|
|
335
350
|
if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
|
|
336
351
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
337
352
|
}
|
|
338
353
|
return endpoint;
|
|
339
354
|
}
|
|
355
|
+
function queryTypeToParams(queryType, ctx) {
|
|
356
|
+
const { checker } = ctx;
|
|
357
|
+
const params = [];
|
|
358
|
+
const props = queryType.getProperties();
|
|
359
|
+
for (const prop of props) {
|
|
360
|
+
const propName = prop.getName();
|
|
361
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
362
|
+
const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
|
|
363
|
+
params.push({
|
|
364
|
+
name: propName,
|
|
365
|
+
in: "query",
|
|
366
|
+
required: !isOptional,
|
|
367
|
+
schema: typeToSchema(propType, ctx)
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return params;
|
|
371
|
+
}
|
|
372
|
+
function formDataTypeToSchema(formDataType, ctx) {
|
|
373
|
+
const { checker } = ctx;
|
|
374
|
+
const properties = {};
|
|
375
|
+
const required = [];
|
|
376
|
+
const props = formDataType.getProperties();
|
|
377
|
+
for (const prop of props) {
|
|
378
|
+
const propName = prop.getName();
|
|
379
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
380
|
+
const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
|
|
381
|
+
const typeName = checker.typeToString(propType);
|
|
382
|
+
if (typeName.includes("File") || typeName.includes("Blob")) {
|
|
383
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
384
|
+
} else if (propType.isUnion()) {
|
|
385
|
+
const hasFile = propType.types.some((t) => {
|
|
386
|
+
const name = checker.typeToString(t);
|
|
387
|
+
return name.includes("File") || name.includes("Blob");
|
|
388
|
+
});
|
|
389
|
+
if (hasFile) {
|
|
390
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
391
|
+
} else {
|
|
392
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
396
|
+
}
|
|
397
|
+
if (!isOptional) {
|
|
398
|
+
required.push(propName);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const schema = {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties
|
|
404
|
+
};
|
|
405
|
+
if (required.length > 0) {
|
|
406
|
+
schema.required = required;
|
|
407
|
+
}
|
|
408
|
+
return schema;
|
|
409
|
+
}
|
|
340
410
|
|
|
341
411
|
// src/generator.ts
|
|
342
412
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
343
|
-
const {
|
|
344
|
-
title = "API",
|
|
345
|
-
version = "1.0.0",
|
|
346
|
-
description,
|
|
347
|
-
baseUrl
|
|
348
|
-
} = options;
|
|
413
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
349
414
|
const paths = {};
|
|
350
415
|
for (const endpoint of endpoints) {
|
|
351
416
|
if (!paths[endpoint.path]) {
|
|
@@ -399,11 +464,15 @@ function createOperation(endpoint) {
|
|
|
399
464
|
}
|
|
400
465
|
};
|
|
401
466
|
}
|
|
467
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
468
|
+
operation.parameters = endpoint.queryParams;
|
|
469
|
+
}
|
|
402
470
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
471
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
403
472
|
operation.requestBody = {
|
|
404
473
|
required: true,
|
|
405
474
|
content: {
|
|
406
|
-
|
|
475
|
+
[contentType]: {
|
|
407
476
|
schema: endpoint.requestBodySchema
|
|
408
477
|
}
|
|
409
478
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -276,16 +276,20 @@ function isEndpointStructure(type) {
|
|
|
276
276
|
propNames.delete("data");
|
|
277
277
|
propNames.delete("body");
|
|
278
278
|
propNames.delete("error");
|
|
279
|
+
propNames.delete("query");
|
|
280
|
+
propNames.delete("formData");
|
|
279
281
|
const remainingProps = [...propNames].filter(
|
|
280
282
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
281
283
|
);
|
|
282
284
|
return remainingProps.length === 0;
|
|
283
285
|
}
|
|
284
|
-
function parseEndpointType(type,
|
|
286
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
285
287
|
const { checker } = ctx;
|
|
286
288
|
let dataType;
|
|
287
289
|
let bodyType;
|
|
288
290
|
let errorType;
|
|
291
|
+
let queryType;
|
|
292
|
+
let formDataType;
|
|
289
293
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
290
294
|
for (const t of typesToCheck) {
|
|
291
295
|
const props = t.getProperties();
|
|
@@ -297,32 +301,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
297
301
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
298
302
|
} else if (name === "error") {
|
|
299
303
|
errorType = checker.getTypeOfSymbol(prop);
|
|
304
|
+
} else if (name === "query") {
|
|
305
|
+
queryType = checker.getTypeOfSymbol(prop);
|
|
306
|
+
} else if (name === "formData") {
|
|
307
|
+
formDataType = checker.getTypeOfSymbol(prop);
|
|
300
308
|
}
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
const endpoint = {
|
|
304
|
-
path:
|
|
312
|
+
path: pathStr,
|
|
305
313
|
method,
|
|
306
314
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
307
315
|
pathParams
|
|
308
316
|
};
|
|
309
|
-
if (
|
|
317
|
+
if (formDataType && !(formDataType.flags & ts2.TypeFlags.Never)) {
|
|
318
|
+
endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
|
|
319
|
+
endpoint.requestBodyContentType = "multipart/form-data";
|
|
320
|
+
} else if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
|
|
310
321
|
endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
|
|
322
|
+
endpoint.requestBodyContentType = "application/json";
|
|
323
|
+
}
|
|
324
|
+
if (queryType && !(queryType.flags & ts2.TypeFlags.Never)) {
|
|
325
|
+
endpoint.queryParams = queryTypeToParams(queryType, ctx);
|
|
311
326
|
}
|
|
312
327
|
if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
|
|
313
328
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
314
329
|
}
|
|
315
330
|
return endpoint;
|
|
316
331
|
}
|
|
332
|
+
function queryTypeToParams(queryType, ctx) {
|
|
333
|
+
const { checker } = ctx;
|
|
334
|
+
const params = [];
|
|
335
|
+
const props = queryType.getProperties();
|
|
336
|
+
for (const prop of props) {
|
|
337
|
+
const propName = prop.getName();
|
|
338
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
339
|
+
const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
|
|
340
|
+
params.push({
|
|
341
|
+
name: propName,
|
|
342
|
+
in: "query",
|
|
343
|
+
required: !isOptional,
|
|
344
|
+
schema: typeToSchema(propType, ctx)
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return params;
|
|
348
|
+
}
|
|
349
|
+
function formDataTypeToSchema(formDataType, ctx) {
|
|
350
|
+
const { checker } = ctx;
|
|
351
|
+
const properties = {};
|
|
352
|
+
const required = [];
|
|
353
|
+
const props = formDataType.getProperties();
|
|
354
|
+
for (const prop of props) {
|
|
355
|
+
const propName = prop.getName();
|
|
356
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
357
|
+
const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
|
|
358
|
+
const typeName = checker.typeToString(propType);
|
|
359
|
+
if (typeName.includes("File") || typeName.includes("Blob")) {
|
|
360
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
361
|
+
} else if (propType.isUnion()) {
|
|
362
|
+
const hasFile = propType.types.some((t) => {
|
|
363
|
+
const name = checker.typeToString(t);
|
|
364
|
+
return name.includes("File") || name.includes("Blob");
|
|
365
|
+
});
|
|
366
|
+
if (hasFile) {
|
|
367
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
368
|
+
} else {
|
|
369
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
373
|
+
}
|
|
374
|
+
if (!isOptional) {
|
|
375
|
+
required.push(propName);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const schema = {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties
|
|
381
|
+
};
|
|
382
|
+
if (required.length > 0) {
|
|
383
|
+
schema.required = required;
|
|
384
|
+
}
|
|
385
|
+
return schema;
|
|
386
|
+
}
|
|
317
387
|
|
|
318
388
|
// src/generator.ts
|
|
319
389
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
320
|
-
const {
|
|
321
|
-
title = "API",
|
|
322
|
-
version = "1.0.0",
|
|
323
|
-
description,
|
|
324
|
-
baseUrl
|
|
325
|
-
} = options;
|
|
390
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
326
391
|
const paths = {};
|
|
327
392
|
for (const endpoint of endpoints) {
|
|
328
393
|
if (!paths[endpoint.path]) {
|
|
@@ -376,11 +441,15 @@ function createOperation(endpoint) {
|
|
|
376
441
|
}
|
|
377
442
|
};
|
|
378
443
|
}
|
|
444
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
445
|
+
operation.parameters = endpoint.queryParams;
|
|
446
|
+
}
|
|
379
447
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
448
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
380
449
|
operation.requestBody = {
|
|
381
450
|
required: true,
|
|
382
451
|
content: {
|
|
383
|
-
|
|
452
|
+
[contentType]: {
|
|
384
453
|
schema: endpoint.requestBodySchema
|
|
385
454
|
}
|
|
386
455
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -14,20 +14,24 @@ type JSONSchema = {
|
|
|
14
14
|
format?: string;
|
|
15
15
|
description?: string;
|
|
16
16
|
};
|
|
17
|
+
type OpenAPIRequestBody = {
|
|
18
|
+
required?: boolean;
|
|
19
|
+
content: {
|
|
20
|
+
"application/json"?: {
|
|
21
|
+
schema: JSONSchema;
|
|
22
|
+
};
|
|
23
|
+
"multipart/form-data"?: {
|
|
24
|
+
schema: JSONSchema;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
17
28
|
type OpenAPIOperation = {
|
|
18
29
|
operationId?: string;
|
|
19
30
|
summary?: string;
|
|
20
31
|
description?: string;
|
|
21
32
|
tags?: string[];
|
|
22
33
|
parameters?: OpenAPIParameter[];
|
|
23
|
-
requestBody?:
|
|
24
|
-
required?: boolean;
|
|
25
|
-
content: {
|
|
26
|
-
"application/json": {
|
|
27
|
-
schema: JSONSchema;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
};
|
|
34
|
+
requestBody?: OpenAPIRequestBody;
|
|
31
35
|
responses: Record<string, {
|
|
32
36
|
description: string;
|
|
33
37
|
content?: {
|
|
@@ -73,6 +77,8 @@ type ParsedEndpoint = {
|
|
|
73
77
|
method: "get" | "post" | "put" | "patch" | "delete";
|
|
74
78
|
responseSchema: JSONSchema;
|
|
75
79
|
requestBodySchema?: JSONSchema;
|
|
80
|
+
requestBodyContentType?: "application/json" | "multipart/form-data";
|
|
81
|
+
queryParams?: OpenAPIParameter[];
|
|
76
82
|
errorSchema?: JSONSchema;
|
|
77
83
|
pathParams: string[];
|
|
78
84
|
};
|
|
@@ -99,4 +105,4 @@ type GeneratorOptions = {
|
|
|
99
105
|
};
|
|
100
106
|
declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
|
|
101
107
|
|
|
102
|
-
export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
|
|
108
|
+
export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPIRequestBody, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,20 +14,24 @@ type JSONSchema = {
|
|
|
14
14
|
format?: string;
|
|
15
15
|
description?: string;
|
|
16
16
|
};
|
|
17
|
+
type OpenAPIRequestBody = {
|
|
18
|
+
required?: boolean;
|
|
19
|
+
content: {
|
|
20
|
+
"application/json"?: {
|
|
21
|
+
schema: JSONSchema;
|
|
22
|
+
};
|
|
23
|
+
"multipart/form-data"?: {
|
|
24
|
+
schema: JSONSchema;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
17
28
|
type OpenAPIOperation = {
|
|
18
29
|
operationId?: string;
|
|
19
30
|
summary?: string;
|
|
20
31
|
description?: string;
|
|
21
32
|
tags?: string[];
|
|
22
33
|
parameters?: OpenAPIParameter[];
|
|
23
|
-
requestBody?:
|
|
24
|
-
required?: boolean;
|
|
25
|
-
content: {
|
|
26
|
-
"application/json": {
|
|
27
|
-
schema: JSONSchema;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
};
|
|
34
|
+
requestBody?: OpenAPIRequestBody;
|
|
31
35
|
responses: Record<string, {
|
|
32
36
|
description: string;
|
|
33
37
|
content?: {
|
|
@@ -73,6 +77,8 @@ type ParsedEndpoint = {
|
|
|
73
77
|
method: "get" | "post" | "put" | "patch" | "delete";
|
|
74
78
|
responseSchema: JSONSchema;
|
|
75
79
|
requestBodySchema?: JSONSchema;
|
|
80
|
+
requestBodyContentType?: "application/json" | "multipart/form-data";
|
|
81
|
+
queryParams?: OpenAPIParameter[];
|
|
76
82
|
errorSchema?: JSONSchema;
|
|
77
83
|
pathParams: string[];
|
|
78
84
|
};
|
|
@@ -99,4 +105,4 @@ type GeneratorOptions = {
|
|
|
99
105
|
};
|
|
100
106
|
declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
|
|
101
107
|
|
|
102
|
-
export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
|
|
108
|
+
export { type CLIOptions, type JSONSchema, type OpenAPIOperation, type OpenAPIParameter, type OpenAPIPathItem, type OpenAPIRequestBody, type OpenAPISpec, type ParsedEndpoint, generateOpenAPISpec, parseSchema };
|
package/dist/index.js
CHANGED
|
@@ -307,16 +307,20 @@ function isEndpointStructure(type) {
|
|
|
307
307
|
propNames.delete("data");
|
|
308
308
|
propNames.delete("body");
|
|
309
309
|
propNames.delete("error");
|
|
310
|
+
propNames.delete("query");
|
|
311
|
+
propNames.delete("formData");
|
|
310
312
|
const remainingProps = [...propNames].filter(
|
|
311
313
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
312
314
|
);
|
|
313
315
|
return remainingProps.length === 0;
|
|
314
316
|
}
|
|
315
|
-
function parseEndpointType(type,
|
|
317
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
316
318
|
const { checker } = ctx;
|
|
317
319
|
let dataType;
|
|
318
320
|
let bodyType;
|
|
319
321
|
let errorType;
|
|
322
|
+
let queryType;
|
|
323
|
+
let formDataType;
|
|
320
324
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
321
325
|
for (const t of typesToCheck) {
|
|
322
326
|
const props = t.getProperties();
|
|
@@ -328,32 +332,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
328
332
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
329
333
|
} else if (name === "error") {
|
|
330
334
|
errorType = checker.getTypeOfSymbol(prop);
|
|
335
|
+
} else if (name === "query") {
|
|
336
|
+
queryType = checker.getTypeOfSymbol(prop);
|
|
337
|
+
} else if (name === "formData") {
|
|
338
|
+
formDataType = checker.getTypeOfSymbol(prop);
|
|
331
339
|
}
|
|
332
340
|
}
|
|
333
341
|
}
|
|
334
342
|
const endpoint = {
|
|
335
|
-
path:
|
|
343
|
+
path: pathStr,
|
|
336
344
|
method,
|
|
337
345
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
338
346
|
pathParams
|
|
339
347
|
};
|
|
340
|
-
if (
|
|
348
|
+
if (formDataType && !(formDataType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
349
|
+
endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
|
|
350
|
+
endpoint.requestBodyContentType = "multipart/form-data";
|
|
351
|
+
} else if (bodyType && !(bodyType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
341
352
|
endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
|
|
353
|
+
endpoint.requestBodyContentType = "application/json";
|
|
354
|
+
}
|
|
355
|
+
if (queryType && !(queryType.flags & import_typescript2.default.TypeFlags.Never)) {
|
|
356
|
+
endpoint.queryParams = queryTypeToParams(queryType, ctx);
|
|
342
357
|
}
|
|
343
358
|
if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
|
|
344
359
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
345
360
|
}
|
|
346
361
|
return endpoint;
|
|
347
362
|
}
|
|
363
|
+
function queryTypeToParams(queryType, ctx) {
|
|
364
|
+
const { checker } = ctx;
|
|
365
|
+
const params = [];
|
|
366
|
+
const props = queryType.getProperties();
|
|
367
|
+
for (const prop of props) {
|
|
368
|
+
const propName = prop.getName();
|
|
369
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
370
|
+
const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
|
|
371
|
+
params.push({
|
|
372
|
+
name: propName,
|
|
373
|
+
in: "query",
|
|
374
|
+
required: !isOptional,
|
|
375
|
+
schema: typeToSchema(propType, ctx)
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return params;
|
|
379
|
+
}
|
|
380
|
+
function formDataTypeToSchema(formDataType, ctx) {
|
|
381
|
+
const { checker } = ctx;
|
|
382
|
+
const properties = {};
|
|
383
|
+
const required = [];
|
|
384
|
+
const props = formDataType.getProperties();
|
|
385
|
+
for (const prop of props) {
|
|
386
|
+
const propName = prop.getName();
|
|
387
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
388
|
+
const isOptional = !!(prop.flags & import_typescript2.default.SymbolFlags.Optional);
|
|
389
|
+
const typeName = checker.typeToString(propType);
|
|
390
|
+
if (typeName.includes("File") || typeName.includes("Blob")) {
|
|
391
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
392
|
+
} else if (propType.isUnion()) {
|
|
393
|
+
const hasFile = propType.types.some((t) => {
|
|
394
|
+
const name = checker.typeToString(t);
|
|
395
|
+
return name.includes("File") || name.includes("Blob");
|
|
396
|
+
});
|
|
397
|
+
if (hasFile) {
|
|
398
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
399
|
+
} else {
|
|
400
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
404
|
+
}
|
|
405
|
+
if (!isOptional) {
|
|
406
|
+
required.push(propName);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const schema = {
|
|
410
|
+
type: "object",
|
|
411
|
+
properties
|
|
412
|
+
};
|
|
413
|
+
if (required.length > 0) {
|
|
414
|
+
schema.required = required;
|
|
415
|
+
}
|
|
416
|
+
return schema;
|
|
417
|
+
}
|
|
348
418
|
|
|
349
419
|
// src/generator.ts
|
|
350
420
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
351
|
-
const {
|
|
352
|
-
title = "API",
|
|
353
|
-
version = "1.0.0",
|
|
354
|
-
description,
|
|
355
|
-
baseUrl
|
|
356
|
-
} = options;
|
|
421
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
357
422
|
const paths = {};
|
|
358
423
|
for (const endpoint of endpoints) {
|
|
359
424
|
if (!paths[endpoint.path]) {
|
|
@@ -407,11 +472,15 @@ function createOperation(endpoint) {
|
|
|
407
472
|
}
|
|
408
473
|
};
|
|
409
474
|
}
|
|
475
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
476
|
+
operation.parameters = endpoint.queryParams;
|
|
477
|
+
}
|
|
410
478
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
479
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
411
480
|
operation.requestBody = {
|
|
412
481
|
required: true,
|
|
413
482
|
content: {
|
|
414
|
-
|
|
483
|
+
[contentType]: {
|
|
415
484
|
schema: endpoint.requestBodySchema
|
|
416
485
|
}
|
|
417
486
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -270,16 +270,20 @@ function isEndpointStructure(type) {
|
|
|
270
270
|
propNames.delete("data");
|
|
271
271
|
propNames.delete("body");
|
|
272
272
|
propNames.delete("error");
|
|
273
|
+
propNames.delete("query");
|
|
274
|
+
propNames.delete("formData");
|
|
273
275
|
const remainingProps = [...propNames].filter(
|
|
274
276
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
275
277
|
);
|
|
276
278
|
return remainingProps.length === 0;
|
|
277
279
|
}
|
|
278
|
-
function parseEndpointType(type,
|
|
280
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
279
281
|
const { checker } = ctx;
|
|
280
282
|
let dataType;
|
|
281
283
|
let bodyType;
|
|
282
284
|
let errorType;
|
|
285
|
+
let queryType;
|
|
286
|
+
let formDataType;
|
|
283
287
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
284
288
|
for (const t of typesToCheck) {
|
|
285
289
|
const props = t.getProperties();
|
|
@@ -291,32 +295,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
291
295
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
292
296
|
} else if (name === "error") {
|
|
293
297
|
errorType = checker.getTypeOfSymbol(prop);
|
|
298
|
+
} else if (name === "query") {
|
|
299
|
+
queryType = checker.getTypeOfSymbol(prop);
|
|
300
|
+
} else if (name === "formData") {
|
|
301
|
+
formDataType = checker.getTypeOfSymbol(prop);
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
}
|
|
297
305
|
const endpoint = {
|
|
298
|
-
path:
|
|
306
|
+
path: pathStr,
|
|
299
307
|
method,
|
|
300
308
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
301
309
|
pathParams
|
|
302
310
|
};
|
|
303
|
-
if (
|
|
311
|
+
if (formDataType && !(formDataType.flags & ts2.TypeFlags.Never)) {
|
|
312
|
+
endpoint.requestBodySchema = formDataTypeToSchema(formDataType, ctx);
|
|
313
|
+
endpoint.requestBodyContentType = "multipart/form-data";
|
|
314
|
+
} else if (bodyType && !(bodyType.flags & ts2.TypeFlags.Never)) {
|
|
304
315
|
endpoint.requestBodySchema = typeToSchema(bodyType, ctx);
|
|
316
|
+
endpoint.requestBodyContentType = "application/json";
|
|
317
|
+
}
|
|
318
|
+
if (queryType && !(queryType.flags & ts2.TypeFlags.Never)) {
|
|
319
|
+
endpoint.queryParams = queryTypeToParams(queryType, ctx);
|
|
305
320
|
}
|
|
306
321
|
if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
|
|
307
322
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
308
323
|
}
|
|
309
324
|
return endpoint;
|
|
310
325
|
}
|
|
326
|
+
function queryTypeToParams(queryType, ctx) {
|
|
327
|
+
const { checker } = ctx;
|
|
328
|
+
const params = [];
|
|
329
|
+
const props = queryType.getProperties();
|
|
330
|
+
for (const prop of props) {
|
|
331
|
+
const propName = prop.getName();
|
|
332
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
333
|
+
const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
|
|
334
|
+
params.push({
|
|
335
|
+
name: propName,
|
|
336
|
+
in: "query",
|
|
337
|
+
required: !isOptional,
|
|
338
|
+
schema: typeToSchema(propType, ctx)
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return params;
|
|
342
|
+
}
|
|
343
|
+
function formDataTypeToSchema(formDataType, ctx) {
|
|
344
|
+
const { checker } = ctx;
|
|
345
|
+
const properties = {};
|
|
346
|
+
const required = [];
|
|
347
|
+
const props = formDataType.getProperties();
|
|
348
|
+
for (const prop of props) {
|
|
349
|
+
const propName = prop.getName();
|
|
350
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
351
|
+
const isOptional = !!(prop.flags & ts2.SymbolFlags.Optional);
|
|
352
|
+
const typeName = checker.typeToString(propType);
|
|
353
|
+
if (typeName.includes("File") || typeName.includes("Blob")) {
|
|
354
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
355
|
+
} else if (propType.isUnion()) {
|
|
356
|
+
const hasFile = propType.types.some((t) => {
|
|
357
|
+
const name = checker.typeToString(t);
|
|
358
|
+
return name.includes("File") || name.includes("Blob");
|
|
359
|
+
});
|
|
360
|
+
if (hasFile) {
|
|
361
|
+
properties[propName] = { type: "string", format: "binary" };
|
|
362
|
+
} else {
|
|
363
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
properties[propName] = typeToSchema(propType, ctx);
|
|
367
|
+
}
|
|
368
|
+
if (!isOptional) {
|
|
369
|
+
required.push(propName);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const schema = {
|
|
373
|
+
type: "object",
|
|
374
|
+
properties
|
|
375
|
+
};
|
|
376
|
+
if (required.length > 0) {
|
|
377
|
+
schema.required = required;
|
|
378
|
+
}
|
|
379
|
+
return schema;
|
|
380
|
+
}
|
|
311
381
|
|
|
312
382
|
// src/generator.ts
|
|
313
383
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
314
|
-
const {
|
|
315
|
-
title = "API",
|
|
316
|
-
version = "1.0.0",
|
|
317
|
-
description,
|
|
318
|
-
baseUrl
|
|
319
|
-
} = options;
|
|
384
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
320
385
|
const paths = {};
|
|
321
386
|
for (const endpoint of endpoints) {
|
|
322
387
|
if (!paths[endpoint.path]) {
|
|
@@ -370,11 +435,15 @@ function createOperation(endpoint) {
|
|
|
370
435
|
}
|
|
371
436
|
};
|
|
372
437
|
}
|
|
438
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
439
|
+
operation.parameters = endpoint.queryParams;
|
|
440
|
+
}
|
|
373
441
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
442
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
374
443
|
operation.requestBody = {
|
|
375
444
|
required: true,
|
|
376
445
|
content: {
|
|
377
|
-
|
|
446
|
+
[contentType]: {
|
|
378
447
|
schema: endpoint.requestBodySchema
|
|
379
448
|
}
|
|
380
449
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enlace-openapi",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"enlace-openapi": "./dist/cli.mjs"
|
|
@@ -15,18 +15,17 @@
|
|
|
15
15
|
"require": "./dist/index.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"dev": "tsup --watch",
|
|
20
|
-
"build": "tsup",
|
|
21
|
-
"typecheck": "tsc --noEmit",
|
|
22
|
-
"lint": "eslint src --max-warnings 0",
|
|
23
|
-
"prepublishOnly": "npm run build && npm run typecheck && npm run lint"
|
|
24
|
-
},
|
|
25
18
|
"dependencies": {
|
|
26
19
|
"commander": "^12.1.0",
|
|
27
20
|
"typescript": "^5.9.2"
|
|
28
21
|
},
|
|
29
22
|
"devDependencies": {
|
|
30
23
|
"@types/node": "^22.19.2"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"lint": "eslint src --max-warnings 0"
|
|
31
30
|
}
|
|
32
|
-
}
|
|
31
|
+
}
|