@qualisero/openapi-endpoint 0.12.3 → 0.14.0

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.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
- const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
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
- const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
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
- const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
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,106 +235,1359 @@ function parseOperationsFromSpec(openapiContent) {
228
235
  operationIds.sort();
229
236
  return { operationIds, operationInfoMap };
230
237
  }
231
- function generateApiOperationsContent(operationIds, operationInfoMap) {
232
- // Generate operationsBase dictionary
233
- const operationsBaseContent = operationIds
234
- .map((id) => {
235
- const info = operationInfoMap[id];
236
- return ` ${id}: {\n path: '${info.path}',\n method: HttpMethod.${info.method},\n },`;
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;
237
649
  })
238
650
  .join('\n');
239
- // Generate OperationId enum content
240
- const operationIdContent = operationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
241
651
  return `// Auto-generated from OpenAPI specification
242
652
  // Do not edit this file manually
243
653
 
244
- import type { operations } from './openapi-types.js'
654
+ ${helperUtility}
245
655
 
246
- export enum HttpMethod {
247
- GET = 'GET',
248
- POST = 'POST',
249
- PUT = 'PUT',
250
- PATCH = 'PATCH',
251
- DELETE = 'DELETE',
252
- HEAD = 'HEAD',
253
- OPTIONS = 'OPTIONS',
254
- TRACE = 'TRACE',
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`);
255
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
256
687
 
257
- // Create the typed structure that combines operations with operation metadata
258
- // This ensures the debug method returns correct values and all operations are properly typed
259
- const operationsBase = {
260
- ${operationsBaseContent}
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
+ }
734
+ // ============================================================================
735
+ // List path computation (ported from openapi-helpers.ts for code-gen time use)
736
+ // ============================================================================
737
+ const PLURAL_ES_SUFFIXES_CLI = ['s', 'x', 'z', 'ch', 'sh', 'o'];
738
+ function pluralizeResourceCli(name) {
739
+ if (name.endsWith('y'))
740
+ return name.slice(0, -1) + 'ies';
741
+ if (PLURAL_ES_SUFFIXES_CLI.some((s) => name.endsWith(s)))
742
+ return name + 'es';
743
+ return name + 's';
744
+ }
745
+ /**
746
+ * Computes the list path for a mutation operation (used for cache invalidation).
747
+ * Returns null if no matching list operation is found.
748
+ */
749
+ function computeListPath(operationId, opInfo, operationMap) {
750
+ const method = opInfo.method;
751
+ if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
752
+ return null;
753
+ const prefixes = {
754
+ [HttpMethod.POST]: 'create',
755
+ [HttpMethod.PUT]: 'update',
756
+ [HttpMethod.PATCH]: 'update',
757
+ [HttpMethod.DELETE]: 'delete',
758
+ };
759
+ const prefix = prefixes[method];
760
+ if (!prefix)
761
+ return null;
762
+ let resourceName = null;
763
+ if (operationId.startsWith(prefix)) {
764
+ const remaining = operationId.slice(prefix.length);
765
+ if (remaining.length > 0 && /^[A-Z]/.test(remaining))
766
+ resourceName = remaining;
767
+ }
768
+ if (resourceName) {
769
+ const tryList = (name) => {
770
+ const listId = `list${name}`;
771
+ if (listId in operationMap && operationMap[listId].method === HttpMethod.GET)
772
+ return operationMap[listId].path;
773
+ return null;
774
+ };
775
+ const found = tryList(resourceName) || tryList(pluralizeResourceCli(resourceName));
776
+ if (found)
777
+ return found;
778
+ }
779
+ // Fallback: strip last path param segment
780
+ const segments = opInfo.path.split('/').filter((s) => s.length > 0);
781
+ if (segments.length >= 2 && /^\{[^}]+\}$/.test(segments[segments.length - 1])) {
782
+ return '/' + segments.slice(0, -1).join('/') + '/';
783
+ }
784
+ return null;
785
+ }
786
+ // ============================================================================
787
+ // New generator: api-client.ts
788
+ // ============================================================================
789
+ /**
790
+ * Generate JSDoc comment for an operation function.
791
+ */
792
+ function _generateOperationJSDoc(operationId, method, apiPath) {
793
+ const methodUpper = method.toUpperCase();
794
+ const isQuery = ['GET', 'HEAD', 'OPTIONS'].includes(methodUpper);
795
+ const lines = ['/**', ` * ${operationId}`, ' * ', ` * ${methodUpper} ${apiPath}`];
796
+ if (isQuery) {
797
+ lines.push(' * ');
798
+ lines.push(' * @param pathParams - Path parameters (reactive)');
799
+ lines.push(' * @param options - Query options (enabled, staleTime, onLoad, etc.)');
800
+ lines.push(' * @returns Query result with data, isLoading, refetch(), etc.');
801
+ }
802
+ else {
803
+ lines.push(' * ');
804
+ lines.push(' * @param pathParams - Path parameters (reactive)');
805
+ lines.push(' * @param options - Mutation options (onSuccess, onError, invalidateOperations, etc.)');
806
+ lines.push(' * @returns Mutation helper with mutate() and mutateAsync() methods');
807
+ }
808
+ lines.push(' */');
809
+ return lines.join('\n');
810
+ }
811
+ function generateApiClientContent(operationMap) {
812
+ const ids = Object.keys(operationMap).sort();
813
+ const QUERY_HTTP = new Set(['GET', 'HEAD', 'OPTIONS']);
814
+ const isQuery = (id) => QUERY_HTTP.has(operationMap[id].method);
815
+ const hasPathParams = (id) => operationMap[id].path.includes('{');
816
+ // Registry for invalidateOperations support
817
+ const registryEntries = ids.map((id) => ` ${id}: { path: '${operationMap[id].path}' },`).join('\n');
818
+ // Generic factory helpers (4 patterns)
819
+ const helpers = `/**
820
+ * Generic query helper for operations without path parameters.
821
+ * @internal
822
+ */
823
+ function _queryNoParams<Op extends AllOps>(
824
+ base: _Config,
825
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
826
+ enums: Record<string, unknown>,
827
+ ) {
828
+ type Response = ApiResponse<Op>
829
+ type QueryParams = ApiQueryParams<Op>
830
+
831
+ const useQuery = (
832
+ options?: QueryOptions<Response, QueryParams>,
833
+ ): QueryReturn<Response, Record<string, never>> =>
834
+ useEndpointQuery<Response, Record<string, never>, QueryParams>(
835
+ { ...base, ...cfg },
836
+ undefined,
837
+ options,
838
+ )
839
+
840
+ return {
841
+ /**
842
+ * Query hook for this operation.
843
+ *
844
+ * Returns an object with:
845
+ * - \`data\`: The response data
846
+ * - \`isLoading\`: Whether the query is loading
847
+ * - \`error\`: Error object if the query failed
848
+ * - \`refetch\`: Function to manually trigger a refetch
849
+ * - \`isPending\`: Alias for isLoading
850
+ * - \`status\`: 'pending' | 'error' | 'success'
851
+ *
852
+ * @param options - Query options (enabled, refetchInterval, etc.)
853
+ * @returns Query result object
854
+ */
855
+ useQuery,
856
+ enums,
857
+ } as const
858
+ }
859
+
860
+ /**
861
+ * Generic query helper for operations with path parameters.
862
+ * @internal
863
+ */
864
+ function _queryWithParams<Op extends AllOps>(
865
+ base: _Config,
866
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
867
+ enums: Record<string, unknown>,
868
+ ) {
869
+ type PathParams = ApiPathParams<Op>
870
+ type PathParamsInput = ApiPathParamsInput<Op>
871
+ type Response = ApiResponse<Op>
872
+ type QueryParams = ApiQueryParams<Op>
873
+
874
+ // Two-overload interface: non-function (exact via object-literal checking) +
875
+ // getter function (exact via NoExcessReturn constraint).
876
+ type _UseQuery = {
877
+ (
878
+ pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
879
+ options?: QueryOptions<Response, QueryParams>,
880
+ ): QueryReturn<Response, PathParams>
881
+ <F extends () => PathParamsInput>(
882
+ pathParams: NoExcessReturn<PathParamsInput, F>,
883
+ options?: QueryOptions<Response, QueryParams>,
884
+ ): QueryReturn<Response, PathParams>
885
+ }
886
+
887
+ const _impl = (
888
+ pathParams: ReactiveOr<PathParamsInput>,
889
+ options?: QueryOptions<Response, QueryParams>,
890
+ ): QueryReturn<Response, PathParams> =>
891
+ useEndpointQuery<Response, PathParams, QueryParams>(
892
+ { ...base, ...cfg },
893
+ pathParams as _PathParamsCast,
894
+ options,
895
+ )
896
+
897
+ return {
898
+ /**
899
+ * Query hook for this operation.
900
+ *
901
+ * Returns an object with:
902
+ * - \`data\`: The response data
903
+ * - \`isLoading\`: Whether the query is loading
904
+ * - \`error\`: Error object if the query failed
905
+ * - \`refetch\`: Function to manually trigger a refetch
906
+ * - \`isPending\`: Alias for isLoading
907
+ * - \`status\`: 'pending' | 'error' | 'success'
908
+ *
909
+ * @param pathParams - Path parameters (object, ref, computed, or getter function)
910
+ * @param options - Query options (enabled, refetchInterval, etc.)
911
+ * @returns Query result object
912
+ */
913
+ useQuery: _impl as _UseQuery,
914
+ enums,
915
+ } as const
916
+ }
917
+
918
+ /**
919
+ * Generic mutation helper for operations without path parameters.
920
+ * @internal
921
+ */
922
+ function _mutationNoParams<Op extends AllOps>(
923
+ base: _Config,
924
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
925
+ enums: Record<string, unknown>,
926
+ ) {
927
+ type RequestBody = ApiRequest<Op>
928
+ type Response = ApiResponse<Op>
929
+ type QueryParams = ApiQueryParams<Op>
930
+
931
+ const useMutation = (
932
+ options?: MutationOptions<Response, Record<string, never>, RequestBody, QueryParams>,
933
+ ): MutationReturn<Response, Record<string, never>, RequestBody, QueryParams> =>
934
+ useEndpointMutation<Response, Record<string, never>, RequestBody, QueryParams>(
935
+ { ...base, ...cfg },
936
+ undefined,
937
+ options,
938
+ )
939
+
940
+ return {
941
+ /**
942
+ * Mutation hook for this operation.
943
+ *
944
+ * Returns an object with:
945
+ * - \`mutate\`: Synchronous mutation function (returns void)
946
+ * - \`mutateAsync\`: Async mutation function (returns Promise)
947
+ * - \`data\`: The response data
948
+ * - \`isLoading\`: Whether the mutation is in progress
949
+ * - \`error\`: Error object if the mutation failed
950
+ * - \`isPending\`: Alias for isLoading
951
+ * - \`status\`: 'idle' | 'pending' | 'error' | 'success'
952
+ *
953
+ * @param options - Mutation options (onSuccess, onError, etc.)
954
+ * @returns Mutation result object
955
+ */
956
+ useMutation,
957
+ enums,
958
+ } as const
959
+ }
960
+
961
+ /**
962
+ * Generic mutation helper for operations with path parameters.
963
+ * @internal
964
+ */
965
+ function _mutationWithParams<Op extends AllOps>(
966
+ base: _Config,
967
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
968
+ enums: Record<string, unknown>,
969
+ ) {
970
+ type PathParams = ApiPathParams<Op>
971
+ type PathParamsInput = ApiPathParamsInput<Op>
972
+ type RequestBody = ApiRequest<Op>
973
+ type Response = ApiResponse<Op>
974
+ type QueryParams = ApiQueryParams<Op>
975
+
976
+ // Two-overload interface: non-function (exact via object-literal checking) +
977
+ // getter function (exact via NoExcessReturn constraint).
978
+ type _UseMutation = {
979
+ (
980
+ pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
981
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
982
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams>
983
+ <F extends () => PathParamsInput>(
984
+ pathParams: NoExcessReturn<PathParamsInput, F>,
985
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
986
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams>
987
+ }
988
+
989
+ const _impl = (
990
+ pathParams: ReactiveOr<PathParamsInput>,
991
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
992
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams> =>
993
+ useEndpointMutation<Response, PathParams, RequestBody, QueryParams>(
994
+ { ...base, ...cfg },
995
+ pathParams as _PathParamsCast,
996
+ options,
997
+ )
998
+
999
+ return {
1000
+ /**
1001
+ * Mutation hook for this operation.
1002
+ *
1003
+ * Returns an object with:
1004
+ * - \`mutate\`: Synchronous mutation function (returns void)
1005
+ * - \`mutateAsync\`: Async mutation function (returns Promise)
1006
+ * - \`data\`: The response data
1007
+ * - \`isLoading\`: Whether the mutation is in progress
1008
+ * - \`error\`: Error object if the mutation failed
1009
+ * - \`isPending\`: Alias for isLoading
1010
+ * - \`status\`: 'idle' | 'pending' | 'error' | 'success'
1011
+ *
1012
+ * @param pathParams - Path parameters (object, ref, computed, or getter function)
1013
+ * @param options - Mutation options (onSuccess, onError, etc.)
1014
+ * @returns Mutation result object
1015
+ */
1016
+ useMutation: _impl as _UseMutation,
1017
+ enums,
1018
+ } as const
1019
+ }`;
1020
+ // createApiClient factory with operation calls
1021
+ const factoryCalls = ids
1022
+ .map((id) => {
1023
+ const op = operationMap[id];
1024
+ const { path: apiPath, method } = op;
1025
+ const listPath = computeListPath(id, op, operationMap);
1026
+ const listPathStr = listPath ? `'${listPath}'` : 'null';
1027
+ const query = isQuery(id);
1028
+ const withParams = hasPathParams(id);
1029
+ const cfg = `{ path: '${apiPath}', method: HttpMethod.${method}, listPath: ${listPathStr} }`;
1030
+ const helper = query
1031
+ ? withParams
1032
+ ? '_queryWithParams'
1033
+ : '_queryNoParams'
1034
+ : withParams
1035
+ ? '_mutationWithParams'
1036
+ : '_mutationNoParams';
1037
+ // Build JSDoc comment
1038
+ const docLines = [];
1039
+ // Summary/description
1040
+ if (op.summary) {
1041
+ docLines.push(op.summary);
1042
+ }
1043
+ if (op.description && op.description !== op.summary) {
1044
+ docLines.push(op.description);
1045
+ }
1046
+ // Path parameters
1047
+ if (op.pathParams && op.pathParams.length > 0) {
1048
+ const paramList = op.pathParams.map((p) => `${p.name}: ${p.type}`).join(', ');
1049
+ docLines.push(`@param pathParams - { ${paramList} }`);
1050
+ }
1051
+ // Request body
1052
+ if (op.requestBodySchema) {
1053
+ docLines.push(`@param body - Request body type: ${op.requestBodySchema}`);
1054
+ }
1055
+ // Response
1056
+ if (op.responseSchema) {
1057
+ docLines.push(`@returns Response type: ${op.responseSchema}`);
1058
+ }
1059
+ const jsDoc = docLines.length > 0 ? `\n /**\n * ${docLines.join('\n * ')}\n */` : '';
1060
+ return `${jsDoc}\n ${id}: ${helper}<'${id}'>(base, ${cfg}, ${id}_enums),`;
1061
+ })
1062
+ .join('');
1063
+ // Enum imports
1064
+ const enumImports = ids.map((id) => ` ${id}_enums,`).join('\n');
1065
+ // Type alias for AllOps
1066
+ const allOpsType = `type AllOps = keyof operations`;
1067
+ return `// Auto-generated from OpenAPI specification - do not edit manually
1068
+ // Use \`createApiClient\` to instantiate a fully-typed API client.
1069
+
1070
+ import type { AxiosInstance } from 'axios'
1071
+ import type { Ref, ComputedRef } from 'vue'
1072
+ import {
1073
+ useEndpointQuery,
1074
+ useEndpointMutation,
1075
+ defaultQueryClient,
1076
+ HttpMethod,
1077
+ type QueryOptions,
1078
+ type MutationOptions,
1079
+ type QueryReturn,
1080
+ type MutationReturn,
1081
+ type ReactiveOr,
1082
+ type NoExcessReturn,
1083
+ type QueryClientLike,
1084
+ type MaybeRefOrGetter,
1085
+ } from '@qualisero/openapi-endpoint'
1086
+
1087
+ import type {
1088
+ ApiResponse,
1089
+ ApiRequest,
1090
+ ApiPathParams,
1091
+ ApiPathParamsInput,
1092
+ ApiQueryParams,
1093
+ operations,
1094
+ } from './api-operations.js'
1095
+
1096
+ import {
1097
+ ${enumImports}
1098
+ } from './api-operations.js'
1099
+
1100
+ // ============================================================================
1101
+ // Operations registry (for invalidateOperations support)
1102
+ // ============================================================================
1103
+
1104
+ const _registry = {
1105
+ ${registryEntries}
261
1106
  } as const
262
1107
 
263
- // Merge with operations type to maintain OpenAPI type information
264
- export const openApiOperations = operationsBase as typeof operationsBase & operations
1108
+ // ============================================================================
1109
+ // Internal config type
1110
+ // ============================================================================
265
1111
 
266
- export type OpenApiOperations = typeof openApiOperations
1112
+ type _Config = {
1113
+ axios: AxiosInstance
1114
+ queryClient: QueryClientLike
1115
+ operationsRegistry: typeof _registry
1116
+ }
1117
+
1118
+ // ============================================================================
1119
+ // Type alias for path params cast (avoids repetition)
1120
+ // ============================================================================
1121
+
1122
+ type _PathParamsCast = MaybeRefOrGetter<Record<string, string | number | undefined> | null | undefined>
1123
+
1124
+ // ============================================================================
1125
+ // Type alias for all operations
1126
+ // ============================================================================
1127
+
1128
+ ${allOpsType}
1129
+
1130
+ // ============================================================================
1131
+ // Shared generic factory helpers (4 patterns)
1132
+ // ============================================================================
1133
+
1134
+ ${helpers}
1135
+
1136
+ // ============================================================================
1137
+ // Public API client factory
1138
+ // ============================================================================
267
1139
 
268
- // Dynamically generate OperationId enum from the operations keys
269
- export const OperationId = {
270
- ${operationIdContent}
271
- } satisfies Record<keyof typeof operationsBase, keyof typeof operationsBase>
1140
+ /**
1141
+ * Create a fully-typed API client.
1142
+ *
1143
+ * Each operation in the spec is a property of the returned object:
1144
+ * - GET/HEAD/OPTIONS → \`api.opName.useQuery(...)\`
1145
+ * - POST/PUT/PATCH/DELETE → \`api.opName.useMutation(...)\`
1146
+ * - All operations → \`api.opName.enums.fieldName.Value\`
1147
+ *
1148
+ * @example
1149
+ * \`\`\`ts
1150
+ * import { createApiClient } from './generated/api-client'
1151
+ * import axios from 'axios'
1152
+ *
1153
+ * const api = createApiClient(axios.create({ baseURL: '/api' }))
1154
+ *
1155
+ * // In a Vue component:
1156
+ * const { data: pets } = api.listPets.useQuery()
1157
+ * const create = api.createPet.useMutation()
1158
+ * create.mutate({ data: { name: 'Fluffy' } })
1159
+ * \`\`\`
1160
+ */
1161
+ export function createApiClient(axios: AxiosInstance, queryClient: QueryClientLike = defaultQueryClient) {
1162
+ const base: _Config = { axios, queryClient, operationsRegistry: _registry }
1163
+ return {
1164
+ ${factoryCalls}
1165
+ } as const
1166
+ }
272
1167
 
273
- // Export the type for TypeScript inference
274
- export type OperationId = keyof OpenApiOperations
1168
+ /** The fully-typed API client instance type. */
1169
+ export type ApiClient = ReturnType<typeof createApiClient>
275
1170
  `;
276
1171
  }
277
- async function generateApiOperations(openapiContent, outputDir) {
278
- console.log('🔨 Generating openapi-typed-operations.ts file...');
279
- const { operationIds, operationInfoMap } = parseOperationsFromSpec(openapiContent);
280
- // Generate TypeScript content
281
- const tsContent = generateApiOperationsContent(operationIds, operationInfoMap);
282
- // Write to output file
283
- const outputPath = path.join(outputDir, 'openapi-typed-operations.ts');
284
- fs.writeFileSync(outputPath, tsContent);
285
- console.log(`✅ Generated openapi-typed-operations file: ${outputPath}`);
286
- console.log(`📊 Found ${operationIds.length} operations`);
1172
+ async function generateApiClientFile(openApiSpec, outputDir, excludePrefix) {
1173
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1174
+ const content = generateApiClientContent(operationMap);
1175
+ fs.writeFileSync(path.join(outputDir, 'api-client.ts'), content);
1176
+ console.log(`✅ Generated api-client.ts (${Object.keys(operationMap).length} operations)`);
287
1177
  }
1178
+ // ============================================================================
288
1179
  function printUsage() {
289
1180
  console.log(`
290
- Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory>
1181
+ Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory> [options]
291
1182
 
292
1183
  Arguments:
293
1184
  openapi-input Path to OpenAPI JSON file or URL to fetch it from
294
1185
  output-directory Directory where generated files will be saved
295
1186
 
1187
+ Options:
1188
+ --exclude-prefix PREFIX Exclude operations with operationId starting with PREFIX
1189
+ (default: '_deprecated')
1190
+ --no-exclude Disable operation exclusion (include all operations)
1191
+ --help, -h Show this help message
1192
+
296
1193
  Examples:
297
1194
  npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated
298
1195
  npx @qualisero/openapi-endpoint https://api.example.com/openapi.json ./src/api
1196
+ npx @qualisero/openapi-endpoint ./api.json ./src/gen --exclude-prefix _internal
1197
+ npx @qualisero/openapi-endpoint ./api.json ./src/gen --no-exclude
299
1198
 
300
1199
  This command will generate:
301
- - openapi-types.ts (TypeScript types from OpenAPI spec)
302
- - openapi-typed-operations.ts (Operation IDs and info for use with this library)
1200
+ - openapi-types.ts (TypeScript types from OpenAPI spec)
1201
+ - api-client.ts (Fully-typed createApiClient factory main file to use)
1202
+ - api-operations.ts (Operations map + type aliases)
1203
+ - api-types.ts (Types namespace for type-only access)
1204
+ - api-enums.ts (Type-safe enum objects from OpenAPI spec)
1205
+ - api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
303
1206
  `);
304
1207
  }
1208
+ // ============================================================================
1209
+ // New helper functions for operation-named API
1210
+ // ============================================================================
1211
+ /**
1212
+ * Parses an already-loaded OpenAPISpec into a map of operationId → OperationInfo.
1213
+ * @param openApiSpec The parsed OpenAPI spec object
1214
+ * @param excludePrefix Operations with this prefix are excluded
1215
+ * @returns Map of operation ID to { path, method }
1216
+ */
1217
+ function buildOperationMap(openApiSpec, excludePrefix) {
1218
+ const map = {};
1219
+ for (const [pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1220
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1221
+ if (!HTTP_METHODS.includes(method))
1222
+ continue;
1223
+ const op = rawOp;
1224
+ if (!op.operationId)
1225
+ continue;
1226
+ if (excludePrefix && op.operationId.startsWith(excludePrefix))
1227
+ continue;
1228
+ // Extract path and query parameters
1229
+ const pathParams = [];
1230
+ const queryParams = [];
1231
+ if (op.parameters) {
1232
+ for (const param of op.parameters) {
1233
+ const type = param.schema?.type || 'string';
1234
+ if (param.in === 'path') {
1235
+ pathParams.push({ name: param.name, type });
1236
+ }
1237
+ else if (param.in === 'query') {
1238
+ queryParams.push({ name: param.name, type });
1239
+ }
1240
+ }
1241
+ }
1242
+ // Extract request body schema
1243
+ let requestBodySchema;
1244
+ const reqBodyRef = op.requestBody?.content?.['application/json']?.schema?.$ref;
1245
+ if (reqBodyRef) {
1246
+ requestBodySchema = reqBodyRef.split('/').pop();
1247
+ }
1248
+ // Extract response schema (from 200/201 responses)
1249
+ let responseSchema;
1250
+ if (op.responses) {
1251
+ for (const statusCode of ['200', '201']) {
1252
+ const resRef = op.responses[statusCode]?.content?.['application/json']?.schema?.$ref;
1253
+ if (resRef) {
1254
+ responseSchema = resRef.split('/').pop();
1255
+ break;
1256
+ }
1257
+ }
1258
+ }
1259
+ map[op.operationId] = {
1260
+ path: pathUrl,
1261
+ method: method.toUpperCase(),
1262
+ summary: op.summary,
1263
+ description: op.description,
1264
+ pathParams: pathParams.length > 0 ? pathParams : undefined,
1265
+ queryParams: queryParams.length > 0 ? queryParams : undefined,
1266
+ requestBodySchema,
1267
+ responseSchema,
1268
+ };
1269
+ }
1270
+ }
1271
+ return map;
1272
+ }
1273
+ /**
1274
+ * Converts an OpenAPI enum array to `{ MemberName: 'value' }`.
1275
+ * @param values Enum values (may include null)
1276
+ * @returns Object with PascalCase keys and string literal values
1277
+ */
1278
+ function enumArrayToObject(values) {
1279
+ const obj = {};
1280
+ for (const v of values) {
1281
+ if (v === null)
1282
+ continue;
1283
+ obj[toEnumMemberName(v)] = String(v);
1284
+ }
1285
+ return obj;
1286
+ }
1287
+ /**
1288
+ * For each operation, extract enum fields from:
1289
+ * 1. Request body object properties (direct `enum` or `$ref` to an enum schema)
1290
+ * 2. Query and path parameters with `enum`
1291
+ * @param openApiSpec The parsed OpenAPI spec
1292
+ * @param operationMap Map from buildOperationMap
1293
+ * @returns operationId → { fieldName → { MemberName: 'value' } }
1294
+ */
1295
+ function buildOperationEnums(openApiSpec, operationMap) {
1296
+ // Schema-level enum lookup: schemaName → { MemberName: value }
1297
+ const schemaEnumLookup = {};
1298
+ if (openApiSpec.components?.schemas) {
1299
+ for (const [schemaName, schema] of Object.entries(openApiSpec.components.schemas)) {
1300
+ if (schema.enum) {
1301
+ schemaEnumLookup[schemaName] = enumArrayToObject(schema.enum);
1302
+ }
1303
+ }
1304
+ }
1305
+ function resolveEnums(schema) {
1306
+ if (schema.enum)
1307
+ return enumArrayToObject(schema.enum);
1308
+ if (typeof schema.$ref === 'string') {
1309
+ const name = schema.$ref.split('/').pop();
1310
+ return schemaEnumLookup[name] ?? null;
1311
+ }
1312
+ return null;
1313
+ }
1314
+ const result = {};
1315
+ for (const [_pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1316
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1317
+ if (!HTTP_METHODS.includes(method))
1318
+ continue;
1319
+ const op = rawOp;
1320
+ if (!op.operationId || !(op.operationId in operationMap))
1321
+ continue;
1322
+ const fields = {};
1323
+ // Request body properties
1324
+ const bodyProps = op.requestBody?.content?.['application/json']?.schema?.properties;
1325
+ if (bodyProps) {
1326
+ for (const [fieldName, fieldSchema] of Object.entries(bodyProps)) {
1327
+ const resolved = resolveEnums(fieldSchema);
1328
+ if (resolved)
1329
+ fields[fieldName] = resolved;
1330
+ }
1331
+ }
1332
+ // Query + path parameters
1333
+ for (const param of op.parameters ?? []) {
1334
+ if (param.schema) {
1335
+ const resolved = resolveEnums(param.schema);
1336
+ if (resolved)
1337
+ fields[param.name] = resolved;
1338
+ }
1339
+ }
1340
+ result[op.operationId] = fields;
1341
+ }
1342
+ }
1343
+ return result;
1344
+ }
1345
+ // ============================================================================
1346
+ // New generators: api-operations.ts
1347
+ // ============================================================================
1348
+ /**
1349
+ * Generates the content for `api-operations.ts` file.
1350
+ * @param operationMap The operation info map
1351
+ * @param opEnums Per-operation enum fields
1352
+ * @param schemaEnumNames Names from api-enums.ts to re-export
1353
+ * @returns Generated TypeScript file content
1354
+ */
1355
+ function generateApiOperationsContent(operationMap, opEnums, schemaEnumNames) {
1356
+ const ids = Object.keys(operationMap).sort();
1357
+ const _queryIds = ids.filter((id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method));
1358
+ const _mutationIds = ids.filter((id) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(operationMap[id].method));
1359
+ // Per-operation enum consts
1360
+ const enumConsts = ids
1361
+ .map((id) => {
1362
+ const fields = opEnums[id] ?? {};
1363
+ const body = Object.entries(fields)
1364
+ .map(([field, vals]) => {
1365
+ const members = Object.entries(vals)
1366
+ .map(([k, v]) => ` ${k}: ${JSON.stringify(v)} as const,`)
1367
+ .join('\n');
1368
+ return ` ${field}: {\n${members}\n } as const,`;
1369
+ })
1370
+ .join('\n');
1371
+ return `export const ${id}_enums = {\n${body}\n} as const`;
1372
+ })
1373
+ .join('\n\n');
1374
+ // Operations map
1375
+ const opEntries = ids
1376
+ .map((id) => ` ${id}: { path: '${operationMap[id].path}', method: HttpMethod.${operationMap[id].method} },`)
1377
+ .join('\n');
1378
+ // Type helpers — now use openapi-typescript `operations` directly (not OpenApiOperations)
1379
+ const typeHelpers = `
1380
+ type AllOps = keyof operations
1381
+
1382
+ /** Response data type for an operation (all fields required). */
1383
+ export type ApiResponse<K extends AllOps> = _ApiResponse<operations, K>
1384
+ /** Response data type - only \`readonly\` fields required. */
1385
+ export type ApiResponseSafe<K extends AllOps> = _ApiResponseSafe<operations, K>
1386
+ /** Request body type. */
1387
+ export type ApiRequest<K extends AllOps> = _ApiRequest<operations, K>
1388
+ /** Path parameters type. */
1389
+ export type ApiPathParams<K extends AllOps> = _ApiPathParams<operations, K>
1390
+ /** Path parameters input type (allows undefined values for reactive resolution). */
1391
+ export type ApiPathParamsInput<K extends AllOps> = _ApiPathParamsInput<operations, K>
1392
+ /** Query parameters type. */
1393
+ export type ApiQueryParams<K extends AllOps> = _ApiQueryParams<operations, K>`;
1394
+ // Re-exports
1395
+ // Use type-only wildcard export to avoid duplicate identifier errors
1396
+ const reExports = schemaEnumNames.length > 0
1397
+ ? schemaEnumNames.map((n) => `export { ${n} } from './api-enums'`).join('\n') +
1398
+ "\nexport type * from './api-enums'"
1399
+ : '// No schema-level enums to re-export';
1400
+ return `// Auto-generated from OpenAPI specification - do not edit manually
1401
+
1402
+ import type { operations } from './openapi-types.js'
1403
+ import { HttpMethod } from '@qualisero/openapi-endpoint'
1404
+ import type {
1405
+ ApiResponse as _ApiResponse,
1406
+ ApiResponseSafe as _ApiResponseSafe,
1407
+ ApiRequest as _ApiRequest,
1408
+ ApiPathParams as _ApiPathParams,
1409
+ ApiPathParamsInput as _ApiPathParamsInput,
1410
+ ApiQueryParams as _ApiQueryParams,
1411
+ } from '@qualisero/openapi-endpoint'
1412
+
1413
+ export type { operations }
1414
+
1415
+ ${reExports}
1416
+
1417
+ // ============================================================================
1418
+ // Per-operation enum values
1419
+ // ============================================================================
1420
+
1421
+ ${enumConsts}
1422
+
1423
+ // ============================================================================
1424
+ // Operations map (kept for inspection / backward compatibility)
1425
+ // ============================================================================
1426
+
1427
+ const operationsBase = {
1428
+ ${opEntries}
1429
+ } as const
1430
+
1431
+ export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
1432
+ export type OpenApiOperations = typeof openApiOperations
1433
+
1434
+ // ============================================================================
1435
+ // Convenience type aliases
1436
+ // ============================================================================
1437
+ ${typeHelpers}
1438
+ `;
1439
+ }
1440
+ /**
1441
+ * Async wrapper for generateApiOperationsContent.
1442
+ */
1443
+ async function generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames) {
1444
+ console.log('🔨 Generating api-operations.ts...');
1445
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1446
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1447
+ const content = generateApiOperationsContent(operationMap, opEnums, schemaEnumNames);
1448
+ fs.writeFileSync(path.join(outputDir, 'api-operations.ts'), content);
1449
+ console.log(`✅ Generated api-operations.ts (${Object.keys(operationMap).length} operations)`);
1450
+ }
1451
+ // ============================================================================
1452
+ // New generators: api-types.ts
1453
+ // ============================================================================
1454
+ /**
1455
+ * Generates the content for `api-types.ts` file.
1456
+ * @param operationMap The operation info map
1457
+ * @param opEnums Per-operation enum fields
1458
+ * @returns Generated TypeScript file content
1459
+ */
1460
+ function generateApiTypesContent(operationMap, opEnums) {
1461
+ const ids = Object.keys(operationMap).sort();
1462
+ const isQuery = (id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method);
1463
+ const namespaces = ids
1464
+ .map((id) => {
1465
+ const query = isQuery(id);
1466
+ const fields = opEnums[id] ?? {};
1467
+ const enumTypes = Object.entries(fields)
1468
+ .map(([fieldName, vals]) => {
1469
+ const typeName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
1470
+ const union = Object.values(vals)
1471
+ .map((v) => `'${v}'`)
1472
+ .join(' | ');
1473
+ return ` /** \`${union}\` */\n export type ${typeName} = ${union}`;
1474
+ })
1475
+ .join('\n');
1476
+ const commonLines = [
1477
+ ` /** Full response type - all fields required. */`,
1478
+ ` export type Response = _ApiResponse<OpenApiOperations, '${id}'>`,
1479
+ ` /** Response type - only \`readonly\` fields required. */`,
1480
+ ` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
1481
+ ];
1482
+ if (!query) {
1483
+ commonLines.push(` /** Request body type. */`, ` export type Request = _ApiRequest<OpenApiOperations, '${id}'>`);
1484
+ }
1485
+ commonLines.push(` /** Path parameters. */`, ` export type PathParams = _ApiPathParams<OpenApiOperations, '${id}'>`, ` /** Query parameters. */`, ` export type QueryParams = _ApiQueryParams<OpenApiOperations, '${id}'>`);
1486
+ const enumNs = enumTypes ? ` export namespace Enums {\n${enumTypes}\n }` : ` export namespace Enums {}`;
1487
+ return ` export namespace ${id} {\n${commonLines.join('\n')}\n${enumNs}\n }`;
1488
+ })
1489
+ .join('\n\n');
1490
+ return `/* eslint-disable */
1491
+ // Auto-generated from OpenAPI specification — do not edit manually
1492
+
1493
+ import type {
1494
+ ApiResponse as _ApiResponse,
1495
+ ApiResponseSafe as _ApiResponseSafe,
1496
+ ApiRequest as _ApiRequest,
1497
+ ApiPathParams as _ApiPathParams,
1498
+ ApiQueryParams as _ApiQueryParams,
1499
+ } from '@qualisero/openapi-endpoint'
1500
+ import type { operations as OpenApiOperations } from './openapi-types.js'
1501
+
1502
+ /**
1503
+ * Type-only namespace for all API operations.
1504
+ *
1505
+ * @example
1506
+ * \`\`\`ts
1507
+ * import type { Types } from './generated/api-types'
1508
+ *
1509
+ * type Pet = Types.getPet.Response
1510
+ * type NewPet = Types.createPet.Request
1511
+ * type PetStatus = Types.createPet.Enums.Status // 'available' | 'pending' | 'adopted'
1512
+ * type Params = Types.getPet.PathParams // { petId: string }
1513
+ * \`\`\`
1514
+ */
1515
+ export namespace Types {
1516
+ ${namespaces}
1517
+ }
1518
+ `;
1519
+ }
1520
+ /**
1521
+ * Async wrapper for generateApiTypesContent.
1522
+ */
1523
+ async function generateApiTypesFile(openApiSpec, outputDir, excludePrefix) {
1524
+ console.log('🔨 Generating api-types.ts...');
1525
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1526
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1527
+ const content = generateApiTypesContent(operationMap, opEnums);
1528
+ fs.writeFileSync(path.join(outputDir, 'api-types.ts'), content);
1529
+ console.log(`✅ Generated api-types.ts`);
1530
+ }
305
1531
  async function main() {
306
1532
  const args = process.argv.slice(2);
307
1533
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
308
1534
  printUsage();
309
1535
  process.exit(0);
310
1536
  }
311
- if (args.length !== 2) {
312
- console.error('❌ Error: Exactly 2 arguments are required');
1537
+ if (args.length < 2) {
1538
+ console.error('❌ Error: At least 2 arguments are required');
313
1539
  printUsage();
314
1540
  process.exit(1);
315
1541
  }
316
- const [openapiInput, outputDir] = args;
1542
+ const [openapiInput, outputDir, ...optionArgs] = args;
1543
+ // Parse options
1544
+ let excludePrefix = '_deprecated'; // default
1545
+ for (let i = 0; i < optionArgs.length; i++) {
1546
+ if (optionArgs[i] === '--no-exclude') {
1547
+ excludePrefix = null;
1548
+ }
1549
+ else if (optionArgs[i] === '--exclude-prefix') {
1550
+ if (i + 1 < optionArgs.length) {
1551
+ excludePrefix = optionArgs[i + 1];
1552
+ i++; // Skip next arg since we consumed it
1553
+ }
1554
+ else {
1555
+ console.error('❌ Error: --exclude-prefix requires a value');
1556
+ printUsage();
1557
+ process.exit(1);
1558
+ }
1559
+ }
1560
+ }
317
1561
  try {
318
1562
  // Ensure output directory exists
319
1563
  if (!fs.existsSync(outputDir)) {
320
1564
  fs.mkdirSync(outputDir, { recursive: true });
321
1565
  console.log(`📁 Created output directory: ${outputDir}`);
322
1566
  }
323
- // Fetch OpenAPI spec content
1567
+ // Log exclusion settings
1568
+ if (excludePrefix) {
1569
+ console.log(`🚫 Excluding operations with operationId prefix: '${excludePrefix}'`);
1570
+ }
1571
+ else {
1572
+ console.log(`✅ Including all operations (no exclusion filter)`);
1573
+ }
1574
+ // Fetch and parse OpenAPI spec once
324
1575
  let openapiContent = await fetchOpenAPISpec(openapiInput);
325
- // Parse spec and add missing operationIds
326
1576
  const openApiSpec = JSON.parse(openapiContent);
1577
+ // Add missing operationIds
327
1578
  addMissingOperationIds(openApiSpec);
328
1579
  openapiContent = JSON.stringify(openApiSpec, null, 2);
329
- // Generate both files
330
- await Promise.all([generateTypes(openapiContent, outputDir), generateApiOperations(openapiContent, outputDir)]);
1580
+ // Collect schema enum names for re-export
1581
+ const schemaEnumNames = extractEnumsFromSpec(openApiSpec).map((e) => e.name);
1582
+ // Generate all files
1583
+ await Promise.all([
1584
+ generateTypes(openapiContent, outputDir),
1585
+ generateApiEnums(openapiContent, outputDir, excludePrefix),
1586
+ generateApiSchemas(openapiContent, outputDir, excludePrefix),
1587
+ generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames),
1588
+ generateApiTypesFile(openApiSpec, outputDir, excludePrefix),
1589
+ generateApiClientFile(openApiSpec, outputDir, excludePrefix),
1590
+ ]);
331
1591
  console.log('🎉 Code generation completed successfully!');
332
1592
  }
333
1593
  catch (error) {