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/dist/cli.mjs ADDED
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { program } from "commander";
5
+ import fs from "fs";
6
+
7
+ // src/parser.ts
8
+ import ts2 from "typescript";
9
+ import path from "path";
10
+
11
+ // src/type-to-schema.ts
12
+ import ts from "typescript";
13
+ function createSchemaContext(checker) {
14
+ return {
15
+ checker,
16
+ schemas: /* @__PURE__ */ new Map(),
17
+ visitedTypes: /* @__PURE__ */ new Set()
18
+ };
19
+ }
20
+ function typeToSchema(type, ctx) {
21
+ const { checker } = ctx;
22
+ if (type.flags & ts.TypeFlags.String) {
23
+ return { type: "string" };
24
+ }
25
+ if (type.flags & ts.TypeFlags.Number) {
26
+ return { type: "number" };
27
+ }
28
+ if (type.flags & ts.TypeFlags.Boolean) {
29
+ return { type: "boolean" };
30
+ }
31
+ if (type.flags & ts.TypeFlags.Null) {
32
+ return { type: "null" };
33
+ }
34
+ if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
35
+ return {};
36
+ }
37
+ if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) {
38
+ return {};
39
+ }
40
+ if (type.flags & ts.TypeFlags.Never) {
41
+ return {};
42
+ }
43
+ if (type.isStringLiteral()) {
44
+ return { type: "string", const: type.value };
45
+ }
46
+ if (type.isNumberLiteral()) {
47
+ return { type: "number", const: type.value };
48
+ }
49
+ if (type.flags & ts.TypeFlags.BooleanLiteral) {
50
+ const intrinsicName = type.intrinsicName;
51
+ return { type: "boolean", const: intrinsicName === "true" };
52
+ }
53
+ if (type.isUnion()) {
54
+ const nonNullTypes = type.types.filter(
55
+ (t) => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
56
+ );
57
+ const hasNull = type.types.some((t) => t.flags & ts.TypeFlags.Null);
58
+ if (nonNullTypes.length === 1 && hasNull) {
59
+ const schema = typeToSchema(nonNullTypes[0], ctx);
60
+ return { ...schema, nullable: true };
61
+ }
62
+ if (nonNullTypes.every((t) => t.isStringLiteral())) {
63
+ return {
64
+ type: "string",
65
+ enum: nonNullTypes.map((t) => t.value)
66
+ };
67
+ }
68
+ if (nonNullTypes.every((t) => t.isNumberLiteral())) {
69
+ return {
70
+ type: "number",
71
+ enum: nonNullTypes.map((t) => t.value)
72
+ };
73
+ }
74
+ return {
75
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
76
+ };
77
+ }
78
+ if (type.isIntersection()) {
79
+ return intersectionTypeToSchema(type, ctx);
80
+ }
81
+ if (checker.isArrayType(type)) {
82
+ const typeArgs = type.typeArguments;
83
+ if (typeArgs?.[0]) {
84
+ return {
85
+ type: "array",
86
+ items: typeToSchema(typeArgs[0], ctx)
87
+ };
88
+ }
89
+ return { type: "array" };
90
+ }
91
+ if (type.flags & ts.TypeFlags.Object) {
92
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
93
+ const typeName = symbol?.getName();
94
+ if (typeName === "Date") {
95
+ return { type: "string", format: "date-time" };
96
+ }
97
+ if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
98
+ if (ctx.visitedTypes.has(type)) {
99
+ return { $ref: `#/components/schemas/${typeName}` };
100
+ }
101
+ if (!ctx.schemas.has(typeName)) {
102
+ ctx.visitedTypes.add(type);
103
+ const schema = objectTypeToSchema(type, ctx);
104
+ ctx.schemas.set(typeName, schema);
105
+ ctx.visitedTypes.delete(type);
106
+ }
107
+ return { $ref: `#/components/schemas/${typeName}` };
108
+ }
109
+ return objectTypeToSchema(type, ctx);
110
+ }
111
+ return {};
112
+ }
113
+ function objectTypeToSchema(type, ctx) {
114
+ const { checker } = ctx;
115
+ const properties = {};
116
+ const required = [];
117
+ const props = type.getProperties();
118
+ for (const prop of props) {
119
+ const propName = prop.getName();
120
+ const propType = checker.getTypeOfSymbol(prop);
121
+ const isOptional = prop.flags & ts.SymbolFlags.Optional;
122
+ properties[propName] = typeToSchema(propType, ctx);
123
+ if (!isOptional) {
124
+ required.push(propName);
125
+ }
126
+ }
127
+ const schema = {
128
+ type: "object",
129
+ properties
130
+ };
131
+ if (required.length > 0) {
132
+ schema.required = required;
133
+ }
134
+ return schema;
135
+ }
136
+ function intersectionTypeToSchema(type, ctx) {
137
+ const { checker } = ctx;
138
+ const properties = {};
139
+ const required = [];
140
+ for (const t of type.types) {
141
+ const props = t.getProperties();
142
+ for (const prop of props) {
143
+ const propName = prop.getName();
144
+ if (propName.startsWith("__")) continue;
145
+ const propType = checker.getTypeOfSymbol(prop);
146
+ const isOptional = prop.flags & ts.SymbolFlags.Optional;
147
+ properties[propName] = typeToSchema(propType, ctx);
148
+ if (!isOptional && !required.includes(propName)) {
149
+ required.push(propName);
150
+ }
151
+ }
152
+ }
153
+ const schema = {
154
+ type: "object",
155
+ properties
156
+ };
157
+ if (required.length > 0) {
158
+ schema.required = required;
159
+ }
160
+ return schema;
161
+ }
162
+
163
+ // src/parser.ts
164
+ var HTTP_METHODS = ["$get", "$post", "$put", "$patch", "$delete"];
165
+ function isHttpMethod(key) {
166
+ return HTTP_METHODS.includes(key);
167
+ }
168
+ function methodKeyToHttp(key) {
169
+ return key.slice(1);
170
+ }
171
+ function parseSchema(schemaFilePath, typeName) {
172
+ const absolutePath = path.resolve(schemaFilePath);
173
+ const schemaDir = path.dirname(absolutePath);
174
+ const configPath = ts2.findConfigFile(schemaDir, ts2.sys.fileExists, "tsconfig.json");
175
+ let compilerOptions = {
176
+ target: ts2.ScriptTarget.ESNext,
177
+ module: ts2.ModuleKind.NodeNext,
178
+ moduleResolution: ts2.ModuleResolutionKind.NodeNext,
179
+ strict: true
180
+ };
181
+ if (configPath) {
182
+ const configFile = ts2.readConfigFile(configPath, ts2.sys.readFile);
183
+ if (!configFile.error) {
184
+ const parsed = ts2.parseJsonConfigFileContent(
185
+ configFile.config,
186
+ ts2.sys,
187
+ path.dirname(configPath)
188
+ );
189
+ compilerOptions = { ...compilerOptions, ...parsed.options };
190
+ }
191
+ }
192
+ const program2 = ts2.createProgram([absolutePath], compilerOptions);
193
+ const checker = program2.getTypeChecker();
194
+ const sourceFile = program2.getSourceFile(absolutePath);
195
+ if (!sourceFile) {
196
+ throw new Error(`Could not find source file: ${absolutePath}`);
197
+ }
198
+ const schemaType = findExportedType(sourceFile, typeName, checker);
199
+ if (!schemaType) {
200
+ throw new Error(`Could not find exported type '${typeName}' in ${schemaFilePath}`);
201
+ }
202
+ const ctx = createSchemaContext(checker);
203
+ const endpoints = [];
204
+ walkSchemaType(schemaType, "", [], ctx, endpoints, checker);
205
+ return {
206
+ endpoints,
207
+ schemas: ctx.schemas
208
+ };
209
+ }
210
+ function findExportedType(sourceFile, typeName, checker) {
211
+ const symbol = checker.getSymbolAtLocation(sourceFile);
212
+ if (!symbol) return void 0;
213
+ const exports = checker.getExportsOfModule(symbol);
214
+ const typeSymbol = exports.find((exp) => exp.getName() === typeName);
215
+ if (!typeSymbol) return void 0;
216
+ const declaredType = checker.getDeclaredTypeOfSymbol(typeSymbol);
217
+ return declaredType;
218
+ }
219
+ function walkSchemaType(type, currentPath, pathParams, ctx, endpoints, checker) {
220
+ const properties = type.getProperties();
221
+ for (const prop of properties) {
222
+ const propName = prop.getName();
223
+ const propType = checker.getTypeOfSymbol(prop);
224
+ if (isHttpMethod(propName)) {
225
+ const endpoint = parseEndpoint(
226
+ propType,
227
+ currentPath || "/",
228
+ methodKeyToHttp(propName),
229
+ pathParams,
230
+ ctx
231
+ );
232
+ endpoints.push(endpoint);
233
+ } else if (propName === "_") {
234
+ const paramName = getParamNameFromPath(currentPath);
235
+ const newPath = `${currentPath}/{${paramName}}`;
236
+ walkSchemaType(propType, newPath, [...pathParams, paramName], ctx, endpoints, checker);
237
+ } else {
238
+ const newPath = `${currentPath}/${propName}`;
239
+ walkSchemaType(propType, newPath, pathParams, ctx, endpoints, checker);
240
+ }
241
+ }
242
+ }
243
+ function getParamNameFromPath(currentPath) {
244
+ const segments = currentPath.split("/").filter(Boolean);
245
+ const lastSegment = segments[segments.length - 1];
246
+ if (lastSegment) {
247
+ const singular = lastSegment.endsWith("s") ? lastSegment.slice(0, -1) : lastSegment;
248
+ return `${singular}Id`;
249
+ }
250
+ return "id";
251
+ }
252
+ function parseEndpoint(type, path2, method, pathParams, ctx) {
253
+ if (type.isIntersection()) {
254
+ for (const t of type.types) {
255
+ if (isEndpointStructure(t)) {
256
+ return parseEndpointType(type, path2, method, pathParams, ctx);
257
+ }
258
+ }
259
+ }
260
+ if (isEndpointStructure(type)) {
261
+ return parseEndpointType(type, path2, method, pathParams, ctx);
262
+ }
263
+ return {
264
+ path: path2,
265
+ method,
266
+ responseSchema: typeToSchema(type, ctx),
267
+ pathParams
268
+ };
269
+ }
270
+ function isEndpointStructure(type) {
271
+ const props = type.getProperties();
272
+ const propNames = new Set(props.map((p) => p.getName()));
273
+ if (!propNames.has("data")) {
274
+ return false;
275
+ }
276
+ propNames.delete("data");
277
+ propNames.delete("body");
278
+ propNames.delete("error");
279
+ propNames.delete("query");
280
+ propNames.delete("formData");
281
+ const remainingProps = [...propNames].filter(
282
+ (name) => !name.startsWith("__@") && !name.includes("Brand")
283
+ );
284
+ return remainingProps.length === 0;
285
+ }
286
+ function parseEndpointType(type, pathStr, method, pathParams, ctx) {
287
+ const { checker } = ctx;
288
+ let dataType;
289
+ let bodyType;
290
+ let errorType;
291
+ let queryType;
292
+ let formDataType;
293
+ const typesToCheck = type.isIntersection() ? type.types : [type];
294
+ for (const t of typesToCheck) {
295
+ const props = t.getProperties();
296
+ for (const prop of props) {
297
+ const name = prop.getName();
298
+ if (name === "data") {
299
+ dataType = checker.getTypeOfSymbol(prop);
300
+ } else if (name === "body") {
301
+ bodyType = checker.getTypeOfSymbol(prop);
302
+ } else if (name === "error") {
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);
308
+ }
309
+ }
310
+ }
311
+ const endpoint = {
312
+ path: pathStr,
313
+ method,
314
+ responseSchema: dataType ? typeToSchema(dataType, ctx) : {},
315
+ pathParams
316
+ };
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)) {
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);
326
+ }
327
+ if (errorType && !(errorType.flags & ts2.TypeFlags.Never) && !(errorType.flags & ts2.TypeFlags.Unknown)) {
328
+ endpoint.errorSchema = typeToSchema(errorType, ctx);
329
+ }
330
+ return endpoint;
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
+ }
387
+
388
+ // src/generator.ts
389
+ function generateOpenAPISpec(endpoints, schemas, options = {}) {
390
+ const { title = "API", version = "1.0.0", description, baseUrl } = options;
391
+ const paths = {};
392
+ for (const endpoint of endpoints) {
393
+ if (!paths[endpoint.path]) {
394
+ paths[endpoint.path] = {};
395
+ }
396
+ const pathItem = paths[endpoint.path];
397
+ const operation = createOperation(endpoint);
398
+ pathItem[endpoint.method] = operation;
399
+ if (endpoint.pathParams.length > 0 && !pathItem.parameters) {
400
+ pathItem.parameters = endpoint.pathParams.map((param) => ({
401
+ name: param,
402
+ in: "path",
403
+ required: true,
404
+ schema: { type: "string" }
405
+ }));
406
+ }
407
+ }
408
+ const spec = {
409
+ openapi: "3.0.0",
410
+ info: {
411
+ title,
412
+ version
413
+ },
414
+ paths
415
+ };
416
+ if (description) {
417
+ spec.info.description = description;
418
+ }
419
+ if (baseUrl) {
420
+ spec.servers = [{ url: baseUrl }];
421
+ }
422
+ if (schemas.size > 0) {
423
+ spec.components = {
424
+ schemas: Object.fromEntries(schemas)
425
+ };
426
+ }
427
+ return spec;
428
+ }
429
+ function createOperation(endpoint) {
430
+ const operation = {
431
+ responses: {
432
+ "200": {
433
+ description: "Successful response"
434
+ }
435
+ }
436
+ };
437
+ if (hasContent(endpoint.responseSchema)) {
438
+ operation.responses["200"].content = {
439
+ "application/json": {
440
+ schema: endpoint.responseSchema
441
+ }
442
+ };
443
+ }
444
+ if (endpoint.queryParams && endpoint.queryParams.length > 0) {
445
+ operation.parameters = endpoint.queryParams;
446
+ }
447
+ if (endpoint.requestBodySchema && hasContent(endpoint.requestBodySchema)) {
448
+ const contentType = endpoint.requestBodyContentType || "application/json";
449
+ operation.requestBody = {
450
+ required: true,
451
+ content: {
452
+ [contentType]: {
453
+ schema: endpoint.requestBodySchema
454
+ }
455
+ }
456
+ };
457
+ }
458
+ if (endpoint.errorSchema && hasContent(endpoint.errorSchema)) {
459
+ operation.responses["400"] = {
460
+ description: "Error response",
461
+ content: {
462
+ "application/json": {
463
+ schema: endpoint.errorSchema
464
+ }
465
+ }
466
+ };
467
+ }
468
+ return operation;
469
+ }
470
+ function hasContent(schema) {
471
+ return Object.keys(schema).length > 0;
472
+ }
473
+
474
+ // src/cli.ts
475
+ 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) => {
476
+ try {
477
+ const { endpoints, schemas } = parseSchema(options.schema, options.type);
478
+ const spec = generateOpenAPISpec(endpoints, schemas, {
479
+ title: options.title,
480
+ version: options.version,
481
+ baseUrl: options.baseUrl
482
+ });
483
+ const output = JSON.stringify(spec, null, 2);
484
+ if (options.output) {
485
+ fs.writeFileSync(options.output, output);
486
+ console.log(`OpenAPI spec written to ${options.output}`);
487
+ } else {
488
+ console.log(output);
489
+ }
490
+ } catch (error) {
491
+ console.error("Error:", error instanceof Error ? error.message : error);
492
+ process.exit(1);
493
+ }
494
+ });
495
+ program.parse();
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  type JSONSchema = {
3
2
  type?: string;
4
3
  items?: JSONSchema;
@@ -15,20 +14,24 @@ type JSONSchema = {
15
14
  format?: string;
16
15
  description?: string;
17
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
+ };
18
28
  type OpenAPIOperation = {
19
29
  operationId?: string;
20
30
  summary?: string;
21
31
  description?: string;
22
32
  tags?: string[];
23
33
  parameters?: OpenAPIParameter[];
24
- requestBody?: {
25
- required?: boolean;
26
- content: {
27
- "application/json": {
28
- schema: JSONSchema;
29
- };
30
- };
31
- };
34
+ requestBody?: OpenAPIRequestBody;
32
35
  responses: Record<string, {
33
36
  description: string;
34
37
  content?: {
@@ -74,6 +77,8 @@ type ParsedEndpoint = {
74
77
  method: "get" | "post" | "put" | "patch" | "delete";
75
78
  responseSchema: JSONSchema;
76
79
  requestBodySchema?: JSONSchema;
80
+ requestBodyContentType?: "application/json" | "multipart/form-data";
81
+ queryParams?: OpenAPIParameter[];
77
82
  errorSchema?: JSONSchema;
78
83
  pathParams: string[];
79
84
  };
@@ -100,4 +105,4 @@ type GeneratorOptions = {
100
105
  };
101
106
  declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
102
107
 
103
- 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
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  type JSONSchema = {
3
2
  type?: string;
4
3
  items?: JSONSchema;
@@ -15,20 +14,24 @@ type JSONSchema = {
15
14
  format?: string;
16
15
  description?: string;
17
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
+ };
18
28
  type OpenAPIOperation = {
19
29
  operationId?: string;
20
30
  summary?: string;
21
31
  description?: string;
22
32
  tags?: string[];
23
33
  parameters?: OpenAPIParameter[];
24
- requestBody?: {
25
- required?: boolean;
26
- content: {
27
- "application/json": {
28
- schema: JSONSchema;
29
- };
30
- };
31
- };
34
+ requestBody?: OpenAPIRequestBody;
32
35
  responses: Record<string, {
33
36
  description: string;
34
37
  content?: {
@@ -74,6 +77,8 @@ type ParsedEndpoint = {
74
77
  method: "get" | "post" | "put" | "patch" | "delete";
75
78
  responseSchema: JSONSchema;
76
79
  requestBodySchema?: JSONSchema;
80
+ requestBodyContentType?: "application/json" | "multipart/form-data";
81
+ queryParams?: OpenAPIParameter[];
77
82
  errorSchema?: JSONSchema;
78
83
  pathParams: string[];
79
84
  };
@@ -100,4 +105,4 @@ type GeneratorOptions = {
100
105
  };
101
106
  declare function generateOpenAPISpec(endpoints: ParsedEndpoint[], schemas: Map<string, JSONSchema>, options?: GeneratorOptions): OpenAPISpec;
102
107
 
103
- 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 };