@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/README.md +135 -252
- package/dist/cli.js +1318 -58
- package/dist/index.d.ts +9 -213
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -143
- package/dist/openapi-helpers.d.ts +4 -17
- package/dist/openapi-helpers.d.ts.map +1 -1
- package/dist/openapi-helpers.js +3 -93
- package/dist/openapi-mutation.d.ts +48 -111
- package/dist/openapi-mutation.d.ts.map +1 -1
- package/dist/openapi-mutation.js +75 -104
- package/dist/openapi-query.d.ts +46 -209
- package/dist/openapi-query.d.ts.map +1 -1
- package/dist/openapi-query.js +50 -88
- package/dist/openapi-utils.d.ts +31 -4
- package/dist/openapi-utils.d.ts.map +1 -1
- package/dist/openapi-utils.js +45 -56
- package/dist/types.d.ts +250 -280
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/package.json +3 -2
- 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
|
|
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,106 +235,1359 @@ function parseOperationsFromSpec(openapiContent) {
|
|
|
228
235
|
operationIds.sort();
|
|
229
236
|
return { operationIds, operationInfoMap };
|
|
230
237
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
654
|
+
${helperUtility}
|
|
245
655
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
//
|
|
264
|
-
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
// Internal config type
|
|
1110
|
+
// ============================================================================
|
|
265
1111
|
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
export type
|
|
1168
|
+
/** The fully-typed API client instance type. */
|
|
1169
|
+
export type ApiClient = ReturnType<typeof createApiClient>
|
|
275
1170
|
`;
|
|
276
1171
|
}
|
|
277
|
-
async function
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
302
|
-
-
|
|
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
|
|
312
|
-
console.error('❌ Error:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
330
|
-
|
|
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) {
|