enlace-openapi 0.0.1-beta.1 → 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 +225 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +518 -0
- package/dist/cli.mjs +495 -0
- package/dist/index.d.mts +15 -10
- package/dist/index.d.ts +15 -10
- package/dist/index.js +82 -39
- package/dist/index.mjs +82 -42
- package/package.json +9 -10
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -35,8 +34,6 @@ __export(src_exports, {
|
|
|
35
34
|
parseSchema: () => parseSchema
|
|
36
35
|
});
|
|
37
36
|
module.exports = __toCommonJS(src_exports);
|
|
38
|
-
var import_commander = require("commander");
|
|
39
|
-
var import_fs = __toESM(require("fs"));
|
|
40
37
|
|
|
41
38
|
// src/parser.ts
|
|
42
39
|
var import_typescript2 = __toESM(require("typescript"));
|
|
@@ -223,9 +220,9 @@ function parseSchema(schemaFilePath, typeName) {
|
|
|
223
220
|
compilerOptions = { ...compilerOptions, ...parsed.options };
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
|
-
const
|
|
227
|
-
const checker =
|
|
228
|
-
const sourceFile =
|
|
223
|
+
const program = import_typescript2.default.createProgram([absolutePath], compilerOptions);
|
|
224
|
+
const checker = program.getTypeChecker();
|
|
225
|
+
const sourceFile = program.getSourceFile(absolutePath);
|
|
229
226
|
if (!sourceFile) {
|
|
230
227
|
throw new Error(`Could not find source file: ${absolutePath}`);
|
|
231
228
|
}
|
|
@@ -310,16 +307,20 @@ function isEndpointStructure(type) {
|
|
|
310
307
|
propNames.delete("data");
|
|
311
308
|
propNames.delete("body");
|
|
312
309
|
propNames.delete("error");
|
|
310
|
+
propNames.delete("query");
|
|
311
|
+
propNames.delete("formData");
|
|
313
312
|
const remainingProps = [...propNames].filter(
|
|
314
313
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
315
314
|
);
|
|
316
315
|
return remainingProps.length === 0;
|
|
317
316
|
}
|
|
318
|
-
function parseEndpointType(type,
|
|
317
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
319
318
|
const { checker } = ctx;
|
|
320
319
|
let dataType;
|
|
321
320
|
let bodyType;
|
|
322
321
|
let errorType;
|
|
322
|
+
let queryType;
|
|
323
|
+
let formDataType;
|
|
323
324
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
324
325
|
for (const t of typesToCheck) {
|
|
325
326
|
const props = t.getProperties();
|
|
@@ -331,32 +332,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
331
332
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
332
333
|
} else if (name === "error") {
|
|
333
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);
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
}
|
|
337
342
|
const endpoint = {
|
|
338
|
-
path:
|
|
343
|
+
path: pathStr,
|
|
339
344
|
method,
|
|
340
345
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
341
346
|
pathParams
|
|
342
347
|
};
|
|
343
|
-
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)) {
|
|
344
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);
|
|
345
357
|
}
|
|
346
358
|
if (errorType && !(errorType.flags & import_typescript2.default.TypeFlags.Never) && !(errorType.flags & import_typescript2.default.TypeFlags.Unknown)) {
|
|
347
359
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
348
360
|
}
|
|
349
361
|
return endpoint;
|
|
350
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
|
+
}
|
|
351
418
|
|
|
352
419
|
// src/generator.ts
|
|
353
420
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
354
|
-
const {
|
|
355
|
-
title = "API",
|
|
356
|
-
version = "1.0.0",
|
|
357
|
-
description,
|
|
358
|
-
baseUrl
|
|
359
|
-
} = options;
|
|
421
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
360
422
|
const paths = {};
|
|
361
423
|
for (const endpoint of endpoints) {
|
|
362
424
|
if (!paths[endpoint.path]) {
|
|
@@ -410,11 +472,15 @@ function createOperation(endpoint) {
|
|
|
410
472
|
}
|
|
411
473
|
};
|
|
412
474
|
}
|
|
475
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
476
|
+
operation.parameters = endpoint.queryParams;
|
|
477
|
+
}
|
|
413
478
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
479
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
414
480
|
operation.requestBody = {
|
|
415
481
|
required: true,
|
|
416
482
|
content: {
|
|
417
|
-
|
|
483
|
+
[contentType]: {
|
|
418
484
|
schema: endpoint.requestBodySchema
|
|
419
485
|
}
|
|
420
486
|
}
|
|
@@ -435,29 +501,6 @@ function createOperation(endpoint) {
|
|
|
435
501
|
function hasContent(schema) {
|
|
436
502
|
return Object.keys(schema).length > 0;
|
|
437
503
|
}
|
|
438
|
-
|
|
439
|
-
// src/index.ts
|
|
440
|
-
import_commander.program.name("enlace-openapi").description("Generate OpenAPI spec from TypeScript API schema").requiredOption("-s, --schema <path>", "Path to TypeScript file containing the schema type").option("-t, --type <name>", "Name of the schema type to use", "ApiSchema").option("-o, --output <path>", "Output file path (default: stdout)").option("--title <title>", "API title for OpenAPI info").option("--version <version>", "API version for OpenAPI info", "1.0.0").option("--base-url <url>", "Base URL for servers array").action((options) => {
|
|
441
|
-
try {
|
|
442
|
-
const { endpoints, schemas } = parseSchema(options.schema, options.type);
|
|
443
|
-
const spec = generateOpenAPISpec(endpoints, schemas, {
|
|
444
|
-
title: options.title,
|
|
445
|
-
version: options.version,
|
|
446
|
-
baseUrl: options.baseUrl
|
|
447
|
-
});
|
|
448
|
-
const output = JSON.stringify(spec, null, 2);
|
|
449
|
-
if (options.output) {
|
|
450
|
-
import_fs.default.writeFileSync(options.output, output);
|
|
451
|
-
console.log(`OpenAPI spec written to ${options.output}`);
|
|
452
|
-
} else {
|
|
453
|
-
console.log(output);
|
|
454
|
-
}
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
457
|
-
process.exit(1);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
import_commander.program.parse();
|
|
461
504
|
// Annotate the CommonJS export names for ESM import in node:
|
|
462
505
|
0 && (module.exports = {
|
|
463
506
|
generateOpenAPISpec,
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import { program } from "commander";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
|
|
7
1
|
// src/parser.ts
|
|
8
2
|
import ts2 from "typescript";
|
|
9
3
|
import path from "path";
|
|
@@ -189,9 +183,9 @@ function parseSchema(schemaFilePath, typeName) {
|
|
|
189
183
|
compilerOptions = { ...compilerOptions, ...parsed.options };
|
|
190
184
|
}
|
|
191
185
|
}
|
|
192
|
-
const
|
|
193
|
-
const checker =
|
|
194
|
-
const sourceFile =
|
|
186
|
+
const program = ts2.createProgram([absolutePath], compilerOptions);
|
|
187
|
+
const checker = program.getTypeChecker();
|
|
188
|
+
const sourceFile = program.getSourceFile(absolutePath);
|
|
195
189
|
if (!sourceFile) {
|
|
196
190
|
throw new Error(`Could not find source file: ${absolutePath}`);
|
|
197
191
|
}
|
|
@@ -276,16 +270,20 @@ function isEndpointStructure(type) {
|
|
|
276
270
|
propNames.delete("data");
|
|
277
271
|
propNames.delete("body");
|
|
278
272
|
propNames.delete("error");
|
|
273
|
+
propNames.delete("query");
|
|
274
|
+
propNames.delete("formData");
|
|
279
275
|
const remainingProps = [...propNames].filter(
|
|
280
276
|
(name) => !name.startsWith("__@") && !name.includes("Brand")
|
|
281
277
|
);
|
|
282
278
|
return remainingProps.length === 0;
|
|
283
279
|
}
|
|
284
|
-
function parseEndpointType(type,
|
|
280
|
+
function parseEndpointType(type, pathStr, method, pathParams, ctx) {
|
|
285
281
|
const { checker } = ctx;
|
|
286
282
|
let dataType;
|
|
287
283
|
let bodyType;
|
|
288
284
|
let errorType;
|
|
285
|
+
let queryType;
|
|
286
|
+
let formDataType;
|
|
289
287
|
const typesToCheck = type.isIntersection() ? type.types : [type];
|
|
290
288
|
for (const t of typesToCheck) {
|
|
291
289
|
const props = t.getProperties();
|
|
@@ -297,32 +295,93 @@ function parseEndpointType(type, path2, method, pathParams, ctx) {
|
|
|
297
295
|
bodyType = checker.getTypeOfSymbol(prop);
|
|
298
296
|
} else if (name === "error") {
|
|
299
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);
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
const endpoint = {
|
|
304
|
-
path:
|
|
306
|
+
path: pathStr,
|
|
305
307
|
method,
|
|
306
308
|
responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
|
|
307
309
|
pathParams
|
|
308
310
|
};
|
|
309
|
-
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)) {
|
|
310
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);
|
|
311
320
|
}
|
|
312
321
|
if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
|
|
313
322
|
endpoint.errorSchema = typeToSchema(errorType, ctx);
|
|
314
323
|
}
|
|
315
324
|
return endpoint;
|
|
316
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
|
+
}
|
|
317
381
|
|
|
318
382
|
// src/generator.ts
|
|
319
383
|
function generateOpenAPISpec(endpoints, schemas, options = {}) {
|
|
320
|
-
const {
|
|
321
|
-
title = "API",
|
|
322
|
-
version = "1.0.0",
|
|
323
|
-
description,
|
|
324
|
-
baseUrl
|
|
325
|
-
} = options;
|
|
384
|
+
const { title = "API", version = "1.0.0", description, baseUrl } = options;
|
|
326
385
|
const paths = {};
|
|
327
386
|
for (const endpoint of endpoints) {
|
|
328
387
|
if (!paths[endpoint.path]) {
|
|
@@ -376,11 +435,15 @@ function createOperation(endpoint) {
|
|
|
376
435
|
}
|
|
377
436
|
};
|
|
378
437
|
}
|
|
438
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
439
|
+
operation.parameters = endpoint.queryParams;
|
|
440
|
+
}
|
|
379
441
|
if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
|
|
442
|
+
const contentType = endpoint.requestBodyContentType || "application/json";
|
|
380
443
|
operation.requestBody = {
|
|
381
444
|
required: true,
|
|
382
445
|
content: {
|
|
383
|
-
|
|
446
|
+
[contentType]: {
|
|
384
447
|
schema: endpoint.requestBodySchema
|
|
385
448
|
}
|
|
386
449
|
}
|
|
@@ -401,29 +464,6 @@ function createOperation(endpoint) {
|
|
|
401
464
|
function hasContent(schema) {
|
|
402
465
|
return Object.keys(schema).length > 0;
|
|
403
466
|
}
|
|
404
|
-
|
|
405
|
-
// src/index.ts
|
|
406
|
-
program.name("enlace-openapi").description("Generate OpenAPI spec from TypeScript API schema").requiredOption("-s, --schema <path>", "Path to TypeScript file containing the schema type").option("-t, --type <name>", "Name of the schema type to use", "ApiSchema").option("-o, --output <path>", "Output file path (default: stdout)").option("--title <title>", "API title for OpenAPI info").option("--version <version>", "API version for OpenAPI info", "1.0.0").option("--base-url <url>", "Base URL for servers array").action((options) => {
|
|
407
|
-
try {
|
|
408
|
-
const { endpoints, schemas } = parseSchema(options.schema, options.type);
|
|
409
|
-
const spec = generateOpenAPISpec(endpoints, schemas, {
|
|
410
|
-
title: options.title,
|
|
411
|
-
version: options.version,
|
|
412
|
-
baseUrl: options.baseUrl
|
|
413
|
-
});
|
|
414
|
-
const output = JSON.stringify(spec, null, 2);
|
|
415
|
-
if (options.output) {
|
|
416
|
-
fs.writeFileSync(options.output, output);
|
|
417
|
-
console.log(`OpenAPI spec written to ${options.output}`);
|
|
418
|
-
} else {
|
|
419
|
-
console.log(output);
|
|
420
|
-
}
|
|
421
|
-
} catch (error) {
|
|
422
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
423
|
-
process.exit(1);
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
program.parse();
|
|
427
467
|
export {
|
|
428
468
|
generateOpenAPISpec,
|
|
429
469
|
parseSchema
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
"enlace-openapi": "./dist/
|
|
6
|
+
"enlace-openapi": "./dist/cli.mjs"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"dist"
|
|
@@ -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
|
+
}
|