@qualisero/openapi-endpoint 0.12.3 → 0.13.2
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 +32 -48
- package/dist/cli.js +740 -25
- package/dist/index.d.ts +15 -192
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -115
- package/dist/openapi-helpers.d.ts.map +1 -1
- package/dist/openapi-helpers.js +70 -38
- package/dist/openapi-mutation.d.ts +76 -108
- package/dist/openapi-mutation.d.ts.map +1 -1
- package/dist/openapi-mutation.js +39 -52
- package/dist/openapi-query.d.ts +73 -208
- package/dist/openapi-query.d.ts.map +1 -1
- package/dist/openapi-query.js +66 -71
- package/dist/openapi-utils.d.ts +30 -4
- package/dist/openapi-utils.d.ts.map +1 -1
- package/dist/openapi-utils.js +45 -56
- package/dist/types.d.ts +411 -197
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -0
- package/package.json +1 -1
- package/dist/openapi-endpoint.d.ts +0 -18
- package/dist/openapi-endpoint.d.ts.map +0 -1
- package/dist/openapi-endpoint.js +0 -24
- package/dist/types-documentation.d.ts +0 -158
- package/dist/types-documentation.d.ts.map +0 -1
- package/dist/types-documentation.js +0 -9
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,12 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
+
import { HttpMethod } from './types.js';
|
|
5
6
|
const execAsync = promisify(exec);
|
|
7
|
+
/**
|
|
8
|
+
* Standard HTTP methods used in OpenAPI specifications.
|
|
9
|
+
*/
|
|
10
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
|
|
6
11
|
async function fetchOpenAPISpec(input) {
|
|
7
12
|
// Check if input is a URL
|
|
8
13
|
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
@@ -174,8 +179,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
|
|
|
174
179
|
// First pass: collect existing operationIds
|
|
175
180
|
Object.entries(openApiSpec.paths).forEach(([_, pathItem]) => {
|
|
176
181
|
Object.entries(pathItem).forEach(([method, op]) => {
|
|
177
|
-
|
|
178
|
-
if (!httpMethods.includes(method.toLowerCase())) {
|
|
182
|
+
if (!HTTP_METHODS.includes(method.toLowerCase())) {
|
|
179
183
|
return;
|
|
180
184
|
}
|
|
181
185
|
if (op.operationId) {
|
|
@@ -186,8 +190,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
|
|
|
186
190
|
// Second pass: generate operationIds for missing ones
|
|
187
191
|
Object.entries(openApiSpec.paths).forEach(([pathUrl, pathItem]) => {
|
|
188
192
|
Object.entries(pathItem).forEach(([method, op]) => {
|
|
189
|
-
|
|
190
|
-
if (!httpMethods.includes(method.toLowerCase())) {
|
|
193
|
+
if (!HTTP_METHODS.includes(method.toLowerCase())) {
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
193
196
|
if (!op.operationId) {
|
|
@@ -200,7 +203,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
|
|
|
200
203
|
});
|
|
201
204
|
});
|
|
202
205
|
}
|
|
203
|
-
function parseOperationsFromSpec(openapiContent) {
|
|
206
|
+
function parseOperationsFromSpec(openapiContent, excludePrefix = '_deprecated') {
|
|
204
207
|
const openApiSpec = JSON.parse(openapiContent);
|
|
205
208
|
if (!openApiSpec.paths) {
|
|
206
209
|
throw new Error('Invalid OpenAPI spec: missing paths');
|
|
@@ -211,12 +214,16 @@ function parseOperationsFromSpec(openapiContent) {
|
|
|
211
214
|
Object.entries(openApiSpec.paths).forEach(([pathUrl, pathItem]) => {
|
|
212
215
|
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
213
216
|
// Skip non-HTTP methods (like parameters)
|
|
214
|
-
|
|
215
|
-
if (!httpMethods.includes(method.toLowerCase())) {
|
|
217
|
+
if (!HTTP_METHODS.includes(method.toLowerCase())) {
|
|
216
218
|
return;
|
|
217
219
|
}
|
|
218
220
|
const op = operation;
|
|
219
221
|
if (op.operationId) {
|
|
222
|
+
// Skip operations with excluded prefix
|
|
223
|
+
if (excludePrefix && op.operationId.startsWith(excludePrefix)) {
|
|
224
|
+
console.log(`⏭️ Excluding operation: ${op.operationId} (matches prefix '${excludePrefix}')`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
220
227
|
operationIds.push(op.operationId);
|
|
221
228
|
operationInfoMap[op.operationId] = {
|
|
222
229
|
path: pathUrl,
|
|
@@ -228,6 +235,502 @@ function parseOperationsFromSpec(openapiContent) {
|
|
|
228
235
|
operationIds.sort();
|
|
229
236
|
return { operationIds, operationInfoMap };
|
|
230
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Converts a string to camelCase or PascalCase.
|
|
240
|
+
* Handles snake_case, kebab-case, space-separated strings, and mixed cases.
|
|
241
|
+
* Single source of truth for case conversion logic.
|
|
242
|
+
*
|
|
243
|
+
* @param str - Input string to convert
|
|
244
|
+
* @param capitalize - If true, returns PascalCase; if false, returns camelCase
|
|
245
|
+
* @returns Converted string in the requested case
|
|
246
|
+
*/
|
|
247
|
+
function toCase(str, capitalize) {
|
|
248
|
+
// If already camelCase or PascalCase, just adjust first letter
|
|
249
|
+
if (/[a-z]/.test(str) && /[A-Z]/.test(str)) {
|
|
250
|
+
return capitalize ? str.charAt(0).toUpperCase() + str.slice(1) : str.charAt(0).toLowerCase() + str.slice(1);
|
|
251
|
+
}
|
|
252
|
+
// Handle snake_case, kebab-case, spaces, etc.
|
|
253
|
+
const parts = str
|
|
254
|
+
.split(/[-_\s]+/)
|
|
255
|
+
.filter((part) => part.length > 0)
|
|
256
|
+
.map((part) => {
|
|
257
|
+
// If this part is already in camelCase, just capitalize the first letter
|
|
258
|
+
if (/[a-z]/.test(part) && /[A-Z]/.test(part)) {
|
|
259
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
260
|
+
}
|
|
261
|
+
// Otherwise, capitalize and lowercase to normalize
|
|
262
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
263
|
+
});
|
|
264
|
+
if (parts.length === 0)
|
|
265
|
+
return str;
|
|
266
|
+
// Apply capitalization rule to first part
|
|
267
|
+
if (!capitalize) {
|
|
268
|
+
parts[0] = parts[0].charAt(0).toLowerCase() + parts[0].slice(1);
|
|
269
|
+
}
|
|
270
|
+
return parts.join('');
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Converts a string to PascalCase.
|
|
274
|
+
* Handles snake_case, kebab-case, space-separated strings, and preserves existing camelCase.
|
|
275
|
+
*/
|
|
276
|
+
function toPascalCase(str) {
|
|
277
|
+
return toCase(str, true);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Converts a string value to a valid TypeScript property name.
|
|
281
|
+
* - Strings that are valid identifiers are used as-is (capitalized)
|
|
282
|
+
* - Invalid identifiers are wrapped in quotes
|
|
283
|
+
* - Numbers are prefixed with underscore
|
|
284
|
+
*/
|
|
285
|
+
function toEnumMemberName(value) {
|
|
286
|
+
if (value === null) {
|
|
287
|
+
return 'Null'; // Handle null enum values
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === 'number') {
|
|
290
|
+
return `_${value}`; // Numbers can't be property names, prefix with underscore
|
|
291
|
+
}
|
|
292
|
+
// Map common operator symbols to readable names
|
|
293
|
+
const operatorMap = {
|
|
294
|
+
'=': 'Equals',
|
|
295
|
+
'!=': 'NotEquals',
|
|
296
|
+
'<': 'LessThan',
|
|
297
|
+
'>': 'GreaterThan',
|
|
298
|
+
'<=': 'LessThanOrEqual',
|
|
299
|
+
'>=': 'GreaterThanOrEqual',
|
|
300
|
+
'!': 'Not',
|
|
301
|
+
'&&': 'And',
|
|
302
|
+
'||': 'Or',
|
|
303
|
+
'+': 'Plus',
|
|
304
|
+
'-': 'Minus',
|
|
305
|
+
'*': 'Multiply',
|
|
306
|
+
'/': 'Divide',
|
|
307
|
+
'%': 'Modulo',
|
|
308
|
+
'^': 'Caret',
|
|
309
|
+
'&': 'Ampersand',
|
|
310
|
+
'|': 'Pipe',
|
|
311
|
+
'~': 'Tilde',
|
|
312
|
+
'<<': 'LeftShift',
|
|
313
|
+
'>>': 'RightShift',
|
|
314
|
+
};
|
|
315
|
+
if (operatorMap[value]) {
|
|
316
|
+
return operatorMap[value];
|
|
317
|
+
}
|
|
318
|
+
// Check if it's a valid TypeScript identifier
|
|
319
|
+
const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
|
|
320
|
+
if (isValidIdentifier) {
|
|
321
|
+
// Capitalize first letter for convention
|
|
322
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
323
|
+
}
|
|
324
|
+
// For non-identifier strings, replace special characters with underscores
|
|
325
|
+
const cleaned = toPascalCase(value.replace(/[^a-zA-Z0-9_$]/g, '_'));
|
|
326
|
+
// If the result is empty or still invalid, prefix with underscore to make it valid
|
|
327
|
+
if (cleaned.length === 0 || !/^[a-zA-Z_$]/.test(cleaned)) {
|
|
328
|
+
return `_Char${value.charCodeAt(0)}`;
|
|
329
|
+
}
|
|
330
|
+
return cleaned;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Helper function to add enum values to the enums list with deduplication.
|
|
334
|
+
* If a duplicate is found, it adds the new name as an alias instead of creating a separate enum.
|
|
335
|
+
*/
|
|
336
|
+
function addEnumIfUnique(enumName, enumValues, sourcePath, enums, seenEnumValues) {
|
|
337
|
+
const valuesKey = JSON.stringify(enumValues.sort());
|
|
338
|
+
// Check if we've seen this exact set of values before
|
|
339
|
+
const existingName = seenEnumValues.get(valuesKey);
|
|
340
|
+
if (existingName) {
|
|
341
|
+
// Find the existing enum and add this as an alias
|
|
342
|
+
const existingEnum = enums.find((e) => e.name === existingName);
|
|
343
|
+
if (existingEnum) {
|
|
344
|
+
if (!existingEnum.aliases) {
|
|
345
|
+
existingEnum.aliases = [];
|
|
346
|
+
}
|
|
347
|
+
existingEnum.aliases.push(enumName);
|
|
348
|
+
console.log(` ↳ Adding alias ${enumName} → ${existingName}`);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
seenEnumValues.set(valuesKey, enumName);
|
|
353
|
+
enums.push({
|
|
354
|
+
name: enumName,
|
|
355
|
+
values: enumValues,
|
|
356
|
+
sourcePath,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Extracts all enums from an OpenAPI spec.
|
|
361
|
+
* Walks through:
|
|
362
|
+
* 1. components.schemas and their properties
|
|
363
|
+
* 2. Operation parameters (query, header, path, cookie)
|
|
364
|
+
* Deduplicates by comparing enum value sets.
|
|
365
|
+
*/
|
|
366
|
+
function extractEnumsFromSpec(openApiSpec) {
|
|
367
|
+
const enums = [];
|
|
368
|
+
const seenEnumValues = new Map(); // Maps JSON stringified values -> enum name (for deduplication)
|
|
369
|
+
// Extract from components.schemas
|
|
370
|
+
if (openApiSpec.components?.schemas) {
|
|
371
|
+
for (const [schemaName, schema] of Object.entries(openApiSpec.components.schemas)) {
|
|
372
|
+
if (!schema.properties)
|
|
373
|
+
continue;
|
|
374
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
375
|
+
if (!propSchema.enum || !Array.isArray(propSchema.enum))
|
|
376
|
+
continue;
|
|
377
|
+
// Filter out null values from enum array
|
|
378
|
+
const enumValues = propSchema.enum.filter((v) => v !== null);
|
|
379
|
+
// Skip if all values were null
|
|
380
|
+
if (enumValues.length === 0)
|
|
381
|
+
continue;
|
|
382
|
+
// Use schema name as-is (already PascalCase), convert property name from snake_case
|
|
383
|
+
const enumName = schemaName + toPascalCase(propName);
|
|
384
|
+
addEnumIfUnique(enumName, enumValues, `components.schemas.${schemaName}.properties.${propName}`, enums, seenEnumValues);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Extract from operation parameters
|
|
389
|
+
if (openApiSpec.paths) {
|
|
390
|
+
for (const [pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
|
|
391
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
392
|
+
// Skip non-HTTP methods
|
|
393
|
+
if (!HTTP_METHODS.includes(method.toLowerCase())) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const op = operation;
|
|
397
|
+
// Check parameters (query, header, path, cookie)
|
|
398
|
+
if (op.parameters && Array.isArray(op.parameters)) {
|
|
399
|
+
for (const param of op.parameters) {
|
|
400
|
+
const paramObj = param;
|
|
401
|
+
const paramName = paramObj.name;
|
|
402
|
+
const paramIn = paramObj.in;
|
|
403
|
+
const paramSchema = paramObj.schema;
|
|
404
|
+
if (!paramName || !paramIn || !paramSchema?.enum)
|
|
405
|
+
continue;
|
|
406
|
+
const enumValues = paramSchema.enum.filter((v) => v !== null);
|
|
407
|
+
if (enumValues.length === 0)
|
|
408
|
+
continue;
|
|
409
|
+
// Create a descriptive name: OperationName + ParamName
|
|
410
|
+
const operationName = op.operationId
|
|
411
|
+
? toPascalCase(op.operationId)
|
|
412
|
+
: toPascalCase(pathUrl.split('/').pop() || 'param');
|
|
413
|
+
const paramNamePascal = toPascalCase(paramName);
|
|
414
|
+
// Rule 1: Don't duplicate suffix if operation name already ends with param name
|
|
415
|
+
let enumName;
|
|
416
|
+
if (operationName.endsWith(paramNamePascal)) {
|
|
417
|
+
enumName = operationName;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
enumName = operationName + paramNamePascal;
|
|
421
|
+
}
|
|
422
|
+
const sourcePath = `paths.${pathUrl}.${method}.parameters[${paramName}]`;
|
|
423
|
+
addEnumIfUnique(enumName, enumValues, sourcePath, enums, seenEnumValues);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Sort by name for consistent output
|
|
430
|
+
enums.sort((a, b) => a.name.localeCompare(b.name));
|
|
431
|
+
// Rule 2: Create short aliases for common suffixes (>2 words, appears >2 times)
|
|
432
|
+
addCommonSuffixAliases(enums);
|
|
433
|
+
return enums;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Rule 2: Analyzes enum names and creates short aliases for common suffixes.
|
|
437
|
+
* Algorithm:
|
|
438
|
+
* 1. Find all suffixes > 2 words that appear 3+ times
|
|
439
|
+
* 2. Sort by number of occurrences (descending)
|
|
440
|
+
* 3. Remove any suffix that is a suffix of a MORE common one
|
|
441
|
+
* 4. Create aliases for remaining suffixes
|
|
442
|
+
*/
|
|
443
|
+
function addCommonSuffixAliases(enums) {
|
|
444
|
+
// Split enum names into words (by capital letters)
|
|
445
|
+
const splitIntoWords = (name) => {
|
|
446
|
+
return name.split(/(?=[A-Z])/).filter((w) => w.length > 0);
|
|
447
|
+
};
|
|
448
|
+
// Collect ALL enum names (primary + aliases)
|
|
449
|
+
const allEnumNames = [];
|
|
450
|
+
for (const enumInfo of enums) {
|
|
451
|
+
allEnumNames.push(enumInfo.name);
|
|
452
|
+
if (enumInfo.aliases) {
|
|
453
|
+
allEnumNames.push(...enumInfo.aliases);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Extract all possible multi-word suffixes from ALL names
|
|
457
|
+
const suffixCounts = new Map(); // suffix -> set of full enum names
|
|
458
|
+
for (const name of allEnumNames) {
|
|
459
|
+
const words = splitIntoWords(name);
|
|
460
|
+
// Try all suffixes with 3+ words
|
|
461
|
+
for (let wordCount = 3; wordCount <= words.length - 1; wordCount++) {
|
|
462
|
+
// -1 to exclude the full name
|
|
463
|
+
const suffix = words.slice(-wordCount).join('');
|
|
464
|
+
if (!suffixCounts.has(suffix)) {
|
|
465
|
+
suffixCounts.set(suffix, new Set());
|
|
466
|
+
}
|
|
467
|
+
suffixCounts.get(suffix).add(name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Step 1: Find suffixes appearing 3+ times
|
|
471
|
+
const commonSuffixes = [];
|
|
472
|
+
for (const [suffix, enumNames] of suffixCounts.entries()) {
|
|
473
|
+
if (enumNames.size > 2) {
|
|
474
|
+
// Skip if this suffix is already present as a primary enum name or alias
|
|
475
|
+
if (allEnumNames.includes(suffix)) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
commonSuffixes.push({
|
|
479
|
+
suffix,
|
|
480
|
+
count: enumNames.size,
|
|
481
|
+
names: Array.from(enumNames),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Step 2: Sort by occurrence count (descending - most common first)
|
|
486
|
+
commonSuffixes.sort((a, b) => b.count - a.count);
|
|
487
|
+
// Step 3: Remove suffixes that are suffixes of MORE common ones
|
|
488
|
+
const filteredSuffixes = [];
|
|
489
|
+
for (const current of commonSuffixes) {
|
|
490
|
+
let shouldKeep = true;
|
|
491
|
+
// Check if this suffix is a suffix of any MORE common suffix already in the filtered list
|
|
492
|
+
for (const existing of filteredSuffixes) {
|
|
493
|
+
if (existing.suffix.endsWith(current.suffix)) {
|
|
494
|
+
// current is a suffix of existing (which is more common)
|
|
495
|
+
shouldKeep = false;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (shouldKeep) {
|
|
500
|
+
filteredSuffixes.push(current);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Step 4: PROMOTE common suffixes to be PRIMARY enum names
|
|
504
|
+
// Process promotions from most common to least common
|
|
505
|
+
const promotions = new Map();
|
|
506
|
+
for (const { suffix, names } of filteredSuffixes) {
|
|
507
|
+
// Find all primary enums that have this suffix (either as primary name or alias)
|
|
508
|
+
const affectedEnums = [];
|
|
509
|
+
for (const name of names) {
|
|
510
|
+
const enumInfo = enums.find((e) => e.name === name || (e.aliases && e.aliases.includes(name)));
|
|
511
|
+
if (enumInfo && !affectedEnums.includes(enumInfo) && !promotions.has(enumInfo)) {
|
|
512
|
+
affectedEnums.push(enumInfo);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (affectedEnums.length === 0)
|
|
516
|
+
continue;
|
|
517
|
+
// Use the first affected enum as the base (it has the values we need)
|
|
518
|
+
const primaryEnum = affectedEnums[0];
|
|
519
|
+
// Collect all names that should become aliases
|
|
520
|
+
const allAliases = new Set();
|
|
521
|
+
for (const enumInfo of affectedEnums) {
|
|
522
|
+
// Add the primary name as an alias
|
|
523
|
+
allAliases.add(enumInfo.name);
|
|
524
|
+
// Add all existing aliases
|
|
525
|
+
if (enumInfo.aliases) {
|
|
526
|
+
enumInfo.aliases.forEach((alias) => allAliases.add(alias));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Remove the suffix itself from aliases (it will be the primary name)
|
|
530
|
+
allAliases.delete(suffix);
|
|
531
|
+
// Record this promotion to apply later
|
|
532
|
+
promotions.set(primaryEnum, {
|
|
533
|
+
newName: suffix,
|
|
534
|
+
allAliases: Array.from(allAliases),
|
|
535
|
+
});
|
|
536
|
+
console.log(` ↳ Promoting ${suffix} to PRIMARY (was ${primaryEnum.name}, ${names.length} occurrences)`);
|
|
537
|
+
// Mark other affected enums for removal
|
|
538
|
+
for (let i = 1; i < affectedEnums.length; i++) {
|
|
539
|
+
promotions.set(affectedEnums[i], { newName: '', allAliases: [] }); // Mark for deletion
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Apply all promotions
|
|
543
|
+
for (const [enumInfo, promotion] of promotions.entries()) {
|
|
544
|
+
if (promotion.newName === '') {
|
|
545
|
+
// Remove this enum (it was consolidated)
|
|
546
|
+
const index = enums.indexOf(enumInfo);
|
|
547
|
+
if (index > -1) {
|
|
548
|
+
enums.splice(index, 1);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Update the enum name and aliases
|
|
553
|
+
enumInfo.name = promotion.newName;
|
|
554
|
+
enumInfo.aliases = promotion.allAliases;
|
|
555
|
+
enumInfo.sourcePath = `common suffix (promoted)`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Generates the content for api-enums.ts file.
|
|
561
|
+
*/
|
|
562
|
+
function generateApiEnumsContent(enums) {
|
|
563
|
+
if (enums.length === 0) {
|
|
564
|
+
return `// Auto-generated from OpenAPI specification
|
|
565
|
+
// Do not edit this file manually
|
|
566
|
+
|
|
567
|
+
// No enums found in the OpenAPI specification
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
// Generate the generic enum helper utility
|
|
571
|
+
const helperUtility = `/**
|
|
572
|
+
* Generic utility for working with enums
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* import { EnumHelper, RequestedValuationType } from './api-enums'
|
|
576
|
+
*
|
|
577
|
+
* // Get all values
|
|
578
|
+
* const allTypes = EnumHelper.values(RequestedValuationType)
|
|
579
|
+
*
|
|
580
|
+
* // Validate a value
|
|
581
|
+
* if (EnumHelper.isValid(RequestedValuationType, userInput)) {
|
|
582
|
+
* // TypeScript knows userInput is RequestedValuationType
|
|
583
|
+
* }
|
|
584
|
+
*
|
|
585
|
+
* // Reverse lookup
|
|
586
|
+
* const key = EnumHelper.getKey(RequestedValuationType, 'cat') // 'Cat'
|
|
587
|
+
*/
|
|
588
|
+
export const EnumHelper = {
|
|
589
|
+
/**
|
|
590
|
+
* Get all enum values as an array
|
|
591
|
+
*/
|
|
592
|
+
values<T extends Record<string, string | number>>(enumObj: T): Array<T[keyof T]> {
|
|
593
|
+
return Object.values(enumObj) as Array<T[keyof T]>
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get all enum keys as an array
|
|
598
|
+
*/
|
|
599
|
+
keys<T extends Record<string, string | number>>(enumObj: T): Array<keyof T> {
|
|
600
|
+
return Object.keys(enumObj) as Array<keyof T>
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Check if a value is valid for the given enum
|
|
605
|
+
*/
|
|
606
|
+
isValid<T extends Record<string, string | number>>(
|
|
607
|
+
enumObj: T,
|
|
608
|
+
value: unknown,
|
|
609
|
+
): value is T[keyof T] {
|
|
610
|
+
return typeof value === 'string' && (Object.values(enumObj) as string[]).includes(value)
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get the enum key from a value (reverse lookup)
|
|
615
|
+
*/
|
|
616
|
+
getKey<T extends Record<string, string | number>>(enumObj: T, value: T[keyof T]): keyof T | undefined {
|
|
617
|
+
const entry = Object.entries(enumObj).find(([_, v]) => v === value)
|
|
618
|
+
return entry?.[0] as keyof T | undefined
|
|
619
|
+
},
|
|
620
|
+
} as const
|
|
621
|
+
`;
|
|
622
|
+
const enumExports = enums
|
|
623
|
+
.map((enumInfo) => {
|
|
624
|
+
const members = enumInfo.values
|
|
625
|
+
.map((value) => {
|
|
626
|
+
const memberName = toEnumMemberName(value);
|
|
627
|
+
const valueStr = typeof value === 'string' ? `'${value}'` : value;
|
|
628
|
+
return ` ${memberName}: ${valueStr} as const,`;
|
|
629
|
+
})
|
|
630
|
+
.join('\n');
|
|
631
|
+
let output = `/**
|
|
632
|
+
* Enum values from ${enumInfo.sourcePath}
|
|
633
|
+
*/
|
|
634
|
+
export const ${enumInfo.name} = {
|
|
635
|
+
${members}
|
|
636
|
+
} as const
|
|
637
|
+
|
|
638
|
+
export type ${enumInfo.name} = typeof ${enumInfo.name}[keyof typeof ${enumInfo.name}]
|
|
639
|
+
`;
|
|
640
|
+
// Generate type aliases for duplicates
|
|
641
|
+
if (enumInfo.aliases && enumInfo.aliases.length > 0) {
|
|
642
|
+
output += '\n// Type aliases for duplicate enum values\n';
|
|
643
|
+
for (const alias of enumInfo.aliases) {
|
|
644
|
+
output += `export const ${alias} = ${enumInfo.name}\n`;
|
|
645
|
+
output += `export type ${alias} = ${enumInfo.name}\n`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return output;
|
|
649
|
+
})
|
|
650
|
+
.join('\n');
|
|
651
|
+
return `// Auto-generated from OpenAPI specification
|
|
652
|
+
// Do not edit this file manually
|
|
653
|
+
|
|
654
|
+
${helperUtility}
|
|
655
|
+
|
|
656
|
+
${enumExports}
|
|
657
|
+
`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Generates the api-enums.ts file from the OpenAPI spec.
|
|
661
|
+
*/
|
|
662
|
+
async function generateApiEnums(openapiContent, outputDir, _excludePrefix = '_deprecated') {
|
|
663
|
+
console.log('🔨 Generating api-enums.ts file...');
|
|
664
|
+
const openApiSpec = JSON.parse(openapiContent);
|
|
665
|
+
const enums = extractEnumsFromSpec(openApiSpec);
|
|
666
|
+
const tsContent = generateApiEnumsContent(enums);
|
|
667
|
+
const outputPath = path.join(outputDir, 'api-enums.ts');
|
|
668
|
+
fs.writeFileSync(outputPath, tsContent);
|
|
669
|
+
console.log(`✅ Generated api-enums file: ${outputPath}`);
|
|
670
|
+
console.log(`📊 Found ${enums.length} unique enums`);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Removes trailing `_schema` or `Schema` suffix from a string (case-insensitive).
|
|
674
|
+
* Examples: `nuts_schema` → `nuts`, `addressSchema` → `address`, `Pet` → `Pet`
|
|
675
|
+
*/
|
|
676
|
+
function removeSchemaSuffix(name) {
|
|
677
|
+
return name.replace(/(_schema|Schema)$/i, '');
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Generates the content for api-schemas.ts file.
|
|
681
|
+
* Creates type aliases for all schema objects with cleaned names.
|
|
682
|
+
*/
|
|
683
|
+
function generateApiSchemasContent(openApiSpec) {
|
|
684
|
+
if (!openApiSpec.components?.schemas || Object.keys(openApiSpec.components.schemas).length === 0) {
|
|
685
|
+
return `// Auto-generated from OpenAPI specification
|
|
686
|
+
// Do not edit this file manually
|
|
687
|
+
|
|
688
|
+
import type { components } from './openapi-types.js'
|
|
689
|
+
|
|
690
|
+
// No schemas found in the OpenAPI specification
|
|
691
|
+
`;
|
|
692
|
+
}
|
|
693
|
+
const header = `// Auto-generated from OpenAPI specification
|
|
694
|
+
// Do not edit this file manually
|
|
695
|
+
|
|
696
|
+
import type { components } from './openapi-types.js'
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Type aliases for schema objects from the API spec.
|
|
700
|
+
* These are references to components['schemas'] for convenient importing.
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* import type { Nuts, Address, BorrowerInfo } from './api-schemas'
|
|
704
|
+
*
|
|
705
|
+
* const nutsData: Nuts = { NUTS_ID: 'BE241', ... }
|
|
706
|
+
*/
|
|
707
|
+
`;
|
|
708
|
+
const schemaExports = Object.keys(openApiSpec.components.schemas)
|
|
709
|
+
.sort()
|
|
710
|
+
.map((schemaName) => {
|
|
711
|
+
// Remove schema suffix and convert to PascalCase
|
|
712
|
+
const cleanedName = removeSchemaSuffix(schemaName);
|
|
713
|
+
const exportedName = toPascalCase(cleanedName);
|
|
714
|
+
// Only add comment if the name changed
|
|
715
|
+
const comment = exportedName !== schemaName ? `// Schema: ${schemaName}\n` : '';
|
|
716
|
+
return `${comment}export type ${exportedName} = components['schemas']['${schemaName}']`;
|
|
717
|
+
})
|
|
718
|
+
.join('\n\n');
|
|
719
|
+
return header + '\n' + schemaExports + '\n';
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Generates the api-schemas.ts file from the OpenAPI spec.
|
|
723
|
+
*/
|
|
724
|
+
async function generateApiSchemas(openapiContent, outputDir, _excludePrefix = '_deprecated') {
|
|
725
|
+
console.log('🔨 Generating api-schemas.ts file...');
|
|
726
|
+
const openApiSpec = JSON.parse(openapiContent);
|
|
727
|
+
const schemaCount = Object.keys(openApiSpec.components?.schemas ?? {}).length;
|
|
728
|
+
const tsContent = generateApiSchemasContent(openApiSpec);
|
|
729
|
+
const outputPath = path.join(outputDir, 'api-schemas.ts');
|
|
730
|
+
fs.writeFileSync(outputPath, tsContent);
|
|
731
|
+
console.log(`✅ Generated api-schemas file: ${outputPath}`);
|
|
732
|
+
console.log(`📊 Found ${schemaCount} schemas`);
|
|
733
|
+
}
|
|
231
734
|
function generateApiOperationsContent(operationIds, operationInfoMap) {
|
|
232
735
|
// Generate operationsBase dictionary
|
|
233
736
|
const operationsBaseContent = operationIds
|
|
@@ -236,12 +739,40 @@ function generateApiOperationsContent(operationIds, operationInfoMap) {
|
|
|
236
739
|
return ` ${id}: {\n path: '${info.path}',\n method: HttpMethod.${info.method},\n },`;
|
|
237
740
|
})
|
|
238
741
|
.join('\n');
|
|
239
|
-
|
|
240
|
-
|
|
742
|
+
const queryOperationIds = operationIds.filter((id) => {
|
|
743
|
+
const method = operationInfoMap[id]?.method;
|
|
744
|
+
return method === HttpMethod.GET || method === HttpMethod.HEAD || method === HttpMethod.OPTIONS;
|
|
745
|
+
});
|
|
746
|
+
const mutationOperationIds = operationIds.filter((id) => {
|
|
747
|
+
const method = operationInfoMap[id]?.method;
|
|
748
|
+
return (method === HttpMethod.POST ||
|
|
749
|
+
method === HttpMethod.PUT ||
|
|
750
|
+
method === HttpMethod.PATCH ||
|
|
751
|
+
method === HttpMethod.DELETE);
|
|
752
|
+
});
|
|
753
|
+
// Generate filtered OperationId enums (source of truth)
|
|
754
|
+
const queryOperationIdContent = queryOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
|
|
755
|
+
const mutationOperationIdContent = mutationOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
|
|
756
|
+
// Generate OpType namespace from BOTH lists
|
|
757
|
+
const opTypeContent = [
|
|
758
|
+
...queryOperationIds.map((id) => ` export type ${id} = typeof QueryOperationId.${id}`),
|
|
759
|
+
...mutationOperationIds.map((id) => ` export type ${id} = typeof MutationOperationId.${id}`),
|
|
760
|
+
].join('\n');
|
|
761
|
+
// Generate pre-computed type alias content for Phase 3B
|
|
762
|
+
const queryParamsContent = queryOperationIds.map((id) => ` ${id}: ApiQueryParams<OpType.${id}>`).join('\n');
|
|
763
|
+
const mutationParamsContent = mutationOperationIds.map((id) => ` ${id}: ApiPathParams<OpType.${id}>`).join('\n');
|
|
764
|
+
const mutationBodyContent = mutationOperationIds.map((id) => ` ${id}: ApiRequest<OpType.${id}>`).join('\n');
|
|
241
765
|
return `// Auto-generated from OpenAPI specification
|
|
242
766
|
// Do not edit this file manually
|
|
243
767
|
|
|
244
768
|
import type { operations } from './openapi-types.js'
|
|
769
|
+
import type {
|
|
770
|
+
ApiResponse as ApiResponseBase,
|
|
771
|
+
ApiResponseSafe as ApiResponseSafeBase,
|
|
772
|
+
ApiRequest as ApiRequestBase,
|
|
773
|
+
ApiPathParams as ApiPathParamsBase,
|
|
774
|
+
ApiQueryParams as ApiQueryParamsBase,
|
|
775
|
+
} from '@qualisero/openapi-endpoint'
|
|
245
776
|
|
|
246
777
|
export enum HttpMethod {
|
|
247
778
|
GET = 'GET',
|
|
@@ -261,22 +792,166 @@ ${operationsBaseContent}
|
|
|
261
792
|
} as const
|
|
262
793
|
|
|
263
794
|
// Merge with operations type to maintain OpenAPI type information
|
|
264
|
-
export const openApiOperations = operationsBase as typeof operationsBase & operations
|
|
795
|
+
export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
|
|
265
796
|
|
|
266
797
|
export type OpenApiOperations = typeof openApiOperations
|
|
267
798
|
|
|
268
|
-
//
|
|
269
|
-
export const
|
|
270
|
-
${
|
|
271
|
-
} satisfies Record<keyof typeof operationsBase, keyof typeof operationsBase
|
|
799
|
+
// Query operations only - use with useQuery() for better autocomplete
|
|
800
|
+
export const QueryOperationId = {
|
|
801
|
+
${queryOperationIdContent}
|
|
802
|
+
} satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
|
|
803
|
+
|
|
804
|
+
export type QueryOperationId = keyof typeof QueryOperationId
|
|
805
|
+
|
|
806
|
+
// Mutation operations only - use with useMutation() for better autocomplete
|
|
807
|
+
export const MutationOperationId = {
|
|
808
|
+
${mutationOperationIdContent}
|
|
809
|
+
} satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
|
|
810
|
+
|
|
811
|
+
export type MutationOperationId = keyof typeof MutationOperationId
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Union type of all operation IDs (queries and mutations).
|
|
815
|
+
* Used for generic type constraints in helper types.
|
|
816
|
+
* @internal
|
|
817
|
+
*/
|
|
818
|
+
export type AllOperationIds = QueryOperationId | MutationOperationId
|
|
819
|
+
|
|
820
|
+
// ============================================================================
|
|
821
|
+
// Type-safe API Helpers - Use OpType.XXX for type-safe access with intellisense
|
|
822
|
+
// ============================================================================
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Response data type for an API operation.
|
|
826
|
+
* All fields are REQUIRED - no null checks needed.
|
|
827
|
+
* @example
|
|
828
|
+
* type Response = ApiResponse<OpType.getPet>
|
|
829
|
+
*/
|
|
830
|
+
export type ApiResponse<K extends AllOperationIds> = ApiResponseBase<OpenApiOperations, K>
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Response data type with safe typing for unreliable backends.
|
|
834
|
+
* Only readonly properties are required; others may be undefined.
|
|
835
|
+
* @example
|
|
836
|
+
* type Response = ApiResponseSafe<OpType.getPet>
|
|
837
|
+
*/
|
|
838
|
+
export type ApiResponseSafe<K extends AllOperationIds> = ApiResponseSafeBase<OpenApiOperations, K>
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Request body type for a mutation operation.
|
|
842
|
+
* @example
|
|
843
|
+
* type Request = ApiRequest<OpType.createPet>
|
|
844
|
+
*/
|
|
845
|
+
export type ApiRequest<K extends AllOperationIds> = ApiRequestBase<OpenApiOperations, K>
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Path parameters type for an operation.
|
|
849
|
+
* @example
|
|
850
|
+
* type Params = ApiPathParams<OpType.getPet>
|
|
851
|
+
*/
|
|
852
|
+
export type ApiPathParams<K extends AllOperationIds> = ApiPathParamsBase<OpenApiOperations, K>
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Query parameters type for an operation.
|
|
856
|
+
* @example
|
|
857
|
+
* type Params = ApiQueryParams<OpType.listPets>
|
|
858
|
+
*/
|
|
859
|
+
export type ApiQueryParams<K extends AllOperationIds> = ApiQueryParamsBase<OpenApiOperations, K>
|
|
860
|
+
|
|
861
|
+
// ============================================================================
|
|
862
|
+
// OpType namespace - enables dot notation: ApiResponse<OpType.getPet>
|
|
863
|
+
// ============================================================================
|
|
272
864
|
|
|
273
|
-
|
|
274
|
-
|
|
865
|
+
/**
|
|
866
|
+
* Namespace that mirrors operation IDs as types.
|
|
867
|
+
* Enables dot notation syntax: ApiResponse<OpType.getPet>
|
|
868
|
+
*
|
|
869
|
+
* This is the idiomatic TypeScript pattern for enabling dot notation
|
|
870
|
+
* on type-level properties. The namespace is preferred over type aliases
|
|
871
|
+
* because it allows \`OpType.getPet\` instead of \`OpType['getPet']\`.
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* type Response = ApiResponse<OpType.getPet>
|
|
875
|
+
* type Request = ApiRequest<OpType.createPet>
|
|
876
|
+
*/
|
|
877
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
878
|
+
export namespace OpType {
|
|
879
|
+
${opTypeContent}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ============================================================================
|
|
883
|
+
// Pre-Computed Type Aliases - For easier DX and clearer intent
|
|
884
|
+
// ============================================================================
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Query parameters for each query operation.
|
|
888
|
+
*
|
|
889
|
+
* Use this to get autocomplete on query parameter names and types.
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* \`\`\`typescript
|
|
893
|
+
* const params: QueryParams['listPets'] = { limit: 10, status: 'available' }
|
|
894
|
+
* const query = api.useQuery(QueryOperationId.listPets, { queryParams: params })
|
|
895
|
+
* \`\`\`
|
|
896
|
+
*/
|
|
897
|
+
export type QueryParams = {
|
|
898
|
+
${queryParamsContent}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Path parameters for each mutation operation.
|
|
903
|
+
*
|
|
904
|
+
* Use this to get autocomplete on path parameter names and types.
|
|
905
|
+
*
|
|
906
|
+
* @example
|
|
907
|
+
* \`\`\`typescript
|
|
908
|
+
* const params: MutationParams['updatePet'] = { petId: '123' }
|
|
909
|
+
* const mutation = api.useMutation(MutationOperationId.updatePet, params)
|
|
910
|
+
* \`\`\`
|
|
911
|
+
*/
|
|
912
|
+
export type MutationParams = {
|
|
913
|
+
${mutationParamsContent}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Request body for each mutation operation.
|
|
918
|
+
*
|
|
919
|
+
* Use this to get autocomplete on request body properties and types.
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* \`\`\`typescript
|
|
923
|
+
* const body: MutationBody['createPet'] = { name: 'Fluffy', species: 'cat' }
|
|
924
|
+
* await createPet.mutateAsync({ data: body })
|
|
925
|
+
* \`\`\`
|
|
926
|
+
*/
|
|
927
|
+
export type MutationBody = {
|
|
928
|
+
${mutationBodyContent}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ============================================================================
|
|
932
|
+
// LEGACY: OperationId (auto-derived from union for backward compatibility)
|
|
933
|
+
// ============================================================================
|
|
934
|
+
// Use QueryOperationId or MutationOperationId directly for better type safety
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* @deprecated Use QueryOperationId or MutationOperationId instead.
|
|
938
|
+
* Auto-derived from their union for backward compatibility.
|
|
939
|
+
*/
|
|
940
|
+
export type OperationId = AllOperationIds
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* @deprecated Use QueryOperationId or MutationOperationId instead.
|
|
944
|
+
* Auto-derived from their union for backward compatibility.
|
|
945
|
+
*/
|
|
946
|
+
export const OperationId = {
|
|
947
|
+
...QueryOperationId,
|
|
948
|
+
...MutationOperationId,
|
|
949
|
+
} satisfies Record<AllOperationIds, AllOperationIds>
|
|
275
950
|
`;
|
|
276
951
|
}
|
|
277
|
-
async function generateApiOperations(openapiContent, outputDir) {
|
|
952
|
+
async function generateApiOperations(openapiContent, outputDir, excludePrefix = '_deprecated') {
|
|
278
953
|
console.log('🔨 Generating openapi-typed-operations.ts file...');
|
|
279
|
-
const { operationIds, operationInfoMap } = parseOperationsFromSpec(openapiContent);
|
|
954
|
+
const { operationIds, operationInfoMap } = parseOperationsFromSpec(openapiContent, excludePrefix);
|
|
280
955
|
// Generate TypeScript content
|
|
281
956
|
const tsContent = generateApiOperationsContent(operationIds, operationInfoMap);
|
|
282
957
|
// Write to output file
|
|
@@ -287,19 +962,29 @@ async function generateApiOperations(openapiContent, outputDir) {
|
|
|
287
962
|
}
|
|
288
963
|
function printUsage() {
|
|
289
964
|
console.log(`
|
|
290
|
-
Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory>
|
|
965
|
+
Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory> [options]
|
|
291
966
|
|
|
292
967
|
Arguments:
|
|
293
968
|
openapi-input Path to OpenAPI JSON file or URL to fetch it from
|
|
294
969
|
output-directory Directory where generated files will be saved
|
|
295
970
|
|
|
971
|
+
Options:
|
|
972
|
+
--exclude-prefix PREFIX Exclude operations with operationId starting with PREFIX
|
|
973
|
+
(default: '_deprecated')
|
|
974
|
+
--no-exclude Disable operation exclusion (include all operations)
|
|
975
|
+
--help, -h Show this help message
|
|
976
|
+
|
|
296
977
|
Examples:
|
|
297
978
|
npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated
|
|
298
979
|
npx @qualisero/openapi-endpoint https://api.example.com/openapi.json ./src/api
|
|
980
|
+
npx @qualisero/openapi-endpoint ./api.json ./src/gen --exclude-prefix _internal
|
|
981
|
+
npx @qualisero/openapi-endpoint ./api.json ./src/gen --no-exclude
|
|
299
982
|
|
|
300
983
|
This command will generate:
|
|
301
|
-
- openapi-types.ts
|
|
984
|
+
- openapi-types.ts (TypeScript types from OpenAPI spec)
|
|
302
985
|
- openapi-typed-operations.ts (Operation IDs and info for use with this library)
|
|
986
|
+
- api-enums.ts (Type-safe enum objects from OpenAPI spec)
|
|
987
|
+
- api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
|
|
303
988
|
`);
|
|
304
989
|
}
|
|
305
990
|
async function main() {
|
|
@@ -308,26 +993,56 @@ async function main() {
|
|
|
308
993
|
printUsage();
|
|
309
994
|
process.exit(0);
|
|
310
995
|
}
|
|
311
|
-
if (args.length
|
|
312
|
-
console.error('❌ Error:
|
|
996
|
+
if (args.length < 2) {
|
|
997
|
+
console.error('❌ Error: At least 2 arguments are required');
|
|
313
998
|
printUsage();
|
|
314
999
|
process.exit(1);
|
|
315
1000
|
}
|
|
316
|
-
const [openapiInput, outputDir] = args;
|
|
1001
|
+
const [openapiInput, outputDir, ...optionArgs] = args;
|
|
1002
|
+
// Parse options
|
|
1003
|
+
let excludePrefix = '_deprecated'; // default
|
|
1004
|
+
for (let i = 0; i < optionArgs.length; i++) {
|
|
1005
|
+
if (optionArgs[i] === '--no-exclude') {
|
|
1006
|
+
excludePrefix = null;
|
|
1007
|
+
}
|
|
1008
|
+
else if (optionArgs[i] === '--exclude-prefix') {
|
|
1009
|
+
if (i + 1 < optionArgs.length) {
|
|
1010
|
+
excludePrefix = optionArgs[i + 1];
|
|
1011
|
+
i++; // Skip next arg since we consumed it
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
console.error('❌ Error: --exclude-prefix requires a value');
|
|
1015
|
+
printUsage();
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
317
1020
|
try {
|
|
318
1021
|
// Ensure output directory exists
|
|
319
1022
|
if (!fs.existsSync(outputDir)) {
|
|
320
1023
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
321
1024
|
console.log(`📁 Created output directory: ${outputDir}`);
|
|
322
1025
|
}
|
|
1026
|
+
// Log exclusion settings
|
|
1027
|
+
if (excludePrefix) {
|
|
1028
|
+
console.log(`🚫 Excluding operations with operationId prefix: '${excludePrefix}'`);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
console.log(`✅ Including all operations (no exclusion filter)`);
|
|
1032
|
+
}
|
|
323
1033
|
// Fetch OpenAPI spec content
|
|
324
1034
|
let openapiContent = await fetchOpenAPISpec(openapiInput);
|
|
325
1035
|
// Parse spec and add missing operationIds
|
|
326
1036
|
const openApiSpec = JSON.parse(openapiContent);
|
|
327
1037
|
addMissingOperationIds(openApiSpec);
|
|
328
1038
|
openapiContent = JSON.stringify(openApiSpec, null, 2);
|
|
329
|
-
// Generate
|
|
330
|
-
await Promise.all([
|
|
1039
|
+
// Generate all files
|
|
1040
|
+
await Promise.all([
|
|
1041
|
+
generateTypes(openapiContent, outputDir),
|
|
1042
|
+
generateApiOperations(openapiContent, outputDir, excludePrefix),
|
|
1043
|
+
generateApiEnums(openapiContent, outputDir, excludePrefix),
|
|
1044
|
+
generateApiSchemas(openapiContent, outputDir, excludePrefix),
|
|
1045
|
+
]);
|
|
331
1046
|
console.log('🎉 Code generation completed successfully!');
|
|
332
1047
|
}
|
|
333
1048
|
catch (error) {
|