@aloma.io/integration-sdk 3.8.55 → 3.8.57
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/MULTI_RESOURCE_GUIDE.md +24 -21
- package/OPENAPI_TO_CONNECTOR.md +146 -16
- package/README.md +62 -10
- package/build/cli.mjs +122 -33
- package/build/internal/dispatcher/index.mjs +3 -2
- package/build/openapi-to-connector.d.mts +88 -11
- package/build/openapi-to-connector.mjs +909 -209
- package/package.json +15 -1
- package/src/cli.mts +140 -37
- package/src/internal/dispatcher/index.mts +4 -2
- package/src/openapi-to-connector.mts +1006 -217
- package/test/scenarios/README.md +148 -0
- package/test/scenarios/complex/expected/controller.mts +271 -0
- package/test/scenarios/complex/expected/orders-resource.mts +264 -0
- package/test/scenarios/complex/expected/products-resource.mts +239 -0
- package/test/scenarios/complex/specs/orders.json +362 -0
- package/test/scenarios/complex/specs/products.json +308 -0
- package/test/scenarios/simple/expected-controller.mts +60 -0
- package/test/scenarios/simple/simple-api.json +39 -0
- package/test/scenarios.test.mts +286 -0
- package/test/verify-scenarios.mjs +298 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import {Command} from 'commander';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import yaml from 'js-yaml';
|
|
7
6
|
import {OpenAPIV3} from 'openapi-types';
|
|
8
7
|
import {z} from 'zod';
|
|
@@ -173,102 +172,6 @@ export class OpenAPIToConnector {
|
|
|
173
172
|
return `${methodPrefix}_${pathSuffix}`;
|
|
174
173
|
}
|
|
175
174
|
|
|
176
|
-
/**
|
|
177
|
-
* Generate JSDoc comment for an operation
|
|
178
|
-
*/
|
|
179
|
-
private generateJSDoc(operation: OperationInfo): string {
|
|
180
|
-
const lines: string[] = [];
|
|
181
|
-
const pathParams: any[] = [];
|
|
182
|
-
const queryParams: any[] = [];
|
|
183
|
-
const hasBody = !!operation.requestBody;
|
|
184
|
-
|
|
185
|
-
if (operation.summary) {
|
|
186
|
-
lines.push(` * ${operation.summary}`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (operation.description) {
|
|
190
|
-
lines.push(` *`);
|
|
191
|
-
// Split long descriptions into multiple lines
|
|
192
|
-
const descLines = operation.description.split('\n');
|
|
193
|
-
descLines.forEach((line) => {
|
|
194
|
-
if (line.trim()) {
|
|
195
|
-
lines.push(` * ${line.trim()}`);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Identify path and query parameters
|
|
201
|
-
if (operation.parameters) {
|
|
202
|
-
for (const param of operation.parameters) {
|
|
203
|
-
if (typeof param === 'object' && 'name' in param && 'in' in param) {
|
|
204
|
-
if (param.in === 'path') {
|
|
205
|
-
pathParams.push(param);
|
|
206
|
-
} else if (param.in === 'query') {
|
|
207
|
-
queryParams.push(param);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check if using simple signature
|
|
214
|
-
const useSimpleSignature = queryParams.length === 0 && !hasBody && pathParams.length <= 1;
|
|
215
|
-
|
|
216
|
-
if (useSimpleSignature && pathParams.length === 1) {
|
|
217
|
-
// Simple signature documentation
|
|
218
|
-
const param = pathParams[0];
|
|
219
|
-
const paramType = param.schema?.type || 'string';
|
|
220
|
-
const paramDesc = param.description || '';
|
|
221
|
-
lines.push(' *');
|
|
222
|
-
lines.push(` * @param {${paramType}} ${param.name} ${paramDesc}`);
|
|
223
|
-
lines.push(` * @param {Object} options (optional) - Request options`);
|
|
224
|
-
lines.push(` * @param {Object} options.headers - Custom headers`);
|
|
225
|
-
} else {
|
|
226
|
-
// Options object documentation
|
|
227
|
-
lines.push(' *');
|
|
228
|
-
|
|
229
|
-
// Check if there are any required parameters
|
|
230
|
-
const hasRequiredParams =
|
|
231
|
-
pathParams.some((p) => p.required) ||
|
|
232
|
-
queryParams.some((p) => p.required) ||
|
|
233
|
-
(operation.requestBody && operation.requestBody.required);
|
|
234
|
-
|
|
235
|
-
const optionsRequired = hasRequiredParams ? '(required)' : '(optional)';
|
|
236
|
-
lines.push(` * @param {Object} options ${optionsRequired} - Request options`);
|
|
237
|
-
|
|
238
|
-
// Document path parameters
|
|
239
|
-
for (const param of pathParams) {
|
|
240
|
-
const paramType = param.schema?.type || 'string';
|
|
241
|
-
const paramDesc = param.description || '';
|
|
242
|
-
const paramRequired = param.required ? '(required)' : '(optional)';
|
|
243
|
-
lines.push(` * @param {${paramType}} options.${param.name} ${paramRequired} - ${paramDesc} [path]`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Document query parameters
|
|
247
|
-
for (const param of queryParams) {
|
|
248
|
-
const paramType = param.schema?.type || 'any';
|
|
249
|
-
const paramDesc = param.description || '';
|
|
250
|
-
const paramRequired = param.required ? '(required)' : '(optional)';
|
|
251
|
-
lines.push(` * @param {${paramType}} options.${param.name} ${paramRequired} - ${paramDesc} [query]`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Document request body
|
|
255
|
-
if (operation.requestBody) {
|
|
256
|
-
const bodyDesc = operation.requestBody.description || 'Request body';
|
|
257
|
-
const required = operation.requestBody.required ? '(required)' : '(optional)';
|
|
258
|
-
lines.push(` * @param {Object} options.body ${required} - ${bodyDesc}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Document headers
|
|
262
|
-
lines.push(` * @param {Object} options.headers (optional) - Custom headers to include in the request`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Document response
|
|
266
|
-
lines.push(' *');
|
|
267
|
-
lines.push(` * @returns {Promise<Object>} ${operation.method} ${operation.path} response`);
|
|
268
|
-
|
|
269
|
-
return lines.join('\n');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
175
|
/**
|
|
273
176
|
* Get the number of operations in the OpenAPI spec
|
|
274
177
|
*/
|
|
@@ -303,13 +206,37 @@ export class OpenAPIToConnector {
|
|
|
303
206
|
}
|
|
304
207
|
}
|
|
305
208
|
|
|
306
|
-
//
|
|
307
|
-
if (
|
|
209
|
+
// Always extract path parameters as discrete parameters when they exist
|
|
210
|
+
if (pathParams.length > 0) {
|
|
308
211
|
const params: string[] = [];
|
|
309
212
|
for (const paramInfo of pathParams) {
|
|
310
213
|
params.push(`${paramInfo.name}: ${paramInfo.type}`);
|
|
311
214
|
}
|
|
312
|
-
|
|
215
|
+
|
|
216
|
+
// Build options object for query params and body
|
|
217
|
+
const optionProps: string[] = [];
|
|
218
|
+
|
|
219
|
+
// Add query parameters to options
|
|
220
|
+
for (const prop of queryParams) {
|
|
221
|
+
const optional = prop.required ? '' : '?';
|
|
222
|
+
optionProps.push(`${prop.name}${optional}: ${prop.type}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Add request body properties directly (flattened)
|
|
226
|
+
if (hasBody) {
|
|
227
|
+
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if options parameter is required (has required query params or required body)
|
|
231
|
+
const hasRequiredNonPathParams =
|
|
232
|
+
queryParams.some((p) => p.required) || (hasBody && operation.requestBody?.required);
|
|
233
|
+
const optionsRequired = hasRequiredNonPathParams ? '' : '?';
|
|
234
|
+
|
|
235
|
+
// Only add options parameter if there are actual options
|
|
236
|
+
if (optionProps.length > 0) {
|
|
237
|
+
params.push(`options${optionsRequired}: {${optionProps.join(', ')}}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
313
240
|
return `(${params.join(', ')})`;
|
|
314
241
|
}
|
|
315
242
|
|
|
@@ -377,14 +304,11 @@ export class OpenAPIToConnector {
|
|
|
377
304
|
optionProps.push(`${objectName}${optional}: {${nestedProps}}`);
|
|
378
305
|
}
|
|
379
306
|
|
|
380
|
-
// Add request body
|
|
307
|
+
// Add request body properties directly (flattened)
|
|
381
308
|
if (hasBody) {
|
|
382
|
-
|
|
309
|
+
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
383
310
|
}
|
|
384
311
|
|
|
385
|
-
// Add custom headers
|
|
386
|
-
optionProps.push('headers?: {[key: string]: any}');
|
|
387
|
-
|
|
388
312
|
// If there are too many parameters, use simplified signature to avoid parsing issues
|
|
389
313
|
// Also check if any parameter name is too long (over 100 chars) which can cause issues
|
|
390
314
|
const hasLongParamNames = optionProps.some((prop) => prop.length > 100);
|
|
@@ -393,44 +317,233 @@ export class OpenAPIToConnector {
|
|
|
393
317
|
return `(options${required}: {[key: string]: any})`;
|
|
394
318
|
}
|
|
395
319
|
|
|
396
|
-
|
|
397
|
-
|
|
320
|
+
// Only add options if there are actual options
|
|
321
|
+
if (optionProps.length > 0) {
|
|
322
|
+
const required = hasRequiredParams ? '' : '?';
|
|
323
|
+
return `(options${required}: {${optionProps.join(', ')}})`;
|
|
324
|
+
} else {
|
|
325
|
+
return '()';
|
|
326
|
+
}
|
|
398
327
|
}
|
|
399
328
|
|
|
400
329
|
/**
|
|
401
|
-
*
|
|
330
|
+
* Resolve a schema reference to a TypeScript type name
|
|
402
331
|
*/
|
|
403
|
-
private
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
332
|
+
private resolveSchemaRef(ref: string): string {
|
|
333
|
+
// Extract the component name from the reference
|
|
334
|
+
// e.g., "#/components/schemas/Company" -> "Company"
|
|
335
|
+
const parts = ref.split('/');
|
|
336
|
+
if (parts.length >= 2) {
|
|
337
|
+
const componentName = parts[parts.length - 1];
|
|
338
|
+
return this.sanitizeTypeName(componentName);
|
|
339
|
+
}
|
|
340
|
+
return 'any';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Sanitize a name to be a valid TypeScript identifier
|
|
345
|
+
*/
|
|
346
|
+
private sanitizeTypeName(name: string): string {
|
|
347
|
+
return (
|
|
348
|
+
name
|
|
349
|
+
// Replace dots with underscores
|
|
350
|
+
.replace(/\./g, '_')
|
|
351
|
+
// Replace + with _Plus (common in OpenAPI for enums)
|
|
352
|
+
.replace(/\+/g, '_Plus')
|
|
353
|
+
// Replace other invalid characters with underscores
|
|
354
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
355
|
+
// Ensure it starts with a letter or underscore
|
|
356
|
+
.replace(/^[0-9]/, '_$&')
|
|
357
|
+
// Remove multiple consecutive underscores
|
|
358
|
+
.replace(/_+/g, '_')
|
|
359
|
+
// Remove trailing/leading underscores
|
|
360
|
+
.replace(/^_+|_+$/g, '') ||
|
|
361
|
+
// Ensure it's not empty
|
|
362
|
+
'UnknownType'
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get TypeScript type from schema object
|
|
368
|
+
*/
|
|
369
|
+
private getTypeFromSchema(schema: any): string {
|
|
370
|
+
if (!schema) return 'any';
|
|
371
|
+
|
|
372
|
+
// Handle $ref
|
|
373
|
+
if (schema.$ref) {
|
|
374
|
+
return this.resolveSchemaRef(schema.$ref);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Handle arrays
|
|
378
|
+
if (schema.type === 'array') {
|
|
379
|
+
if (schema.items) {
|
|
380
|
+
const itemType = this.getTypeFromSchema(schema.items);
|
|
381
|
+
return `${itemType}[]`;
|
|
382
|
+
}
|
|
383
|
+
return 'any[]';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Handle objects with properties
|
|
387
|
+
if (schema.type === 'object' && schema.properties) {
|
|
388
|
+
const propNames = Object.keys(schema.properties);
|
|
389
|
+
|
|
390
|
+
// For response objects, generate inline type definitions
|
|
391
|
+
if (propNames.length <= 5) {
|
|
392
|
+
// Reasonable limit for inline types
|
|
393
|
+
const propTypes = Object.entries(schema.properties).map(([key, prop]: [string, any]) => {
|
|
394
|
+
const propType = this.getTypeFromSchema(prop);
|
|
395
|
+
return `${key}: ${propType}`;
|
|
396
|
+
});
|
|
397
|
+
return `{${propTypes.join('; ')}}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// For complex objects, return a generic object type
|
|
401
|
+
return 'any';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Handle other primitive types
|
|
405
|
+
if (schema.type) {
|
|
406
|
+
switch (schema.type) {
|
|
407
|
+
case 'string':
|
|
408
|
+
return 'string';
|
|
409
|
+
case 'integer':
|
|
410
|
+
case 'number':
|
|
411
|
+
return 'number';
|
|
412
|
+
case 'boolean':
|
|
413
|
+
return 'boolean';
|
|
414
|
+
case 'object':
|
|
415
|
+
return 'any';
|
|
416
|
+
default:
|
|
417
|
+
return 'any';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle enum
|
|
422
|
+
if (schema.enum) {
|
|
423
|
+
return 'string'; // Could be expanded to union types
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Handle allOf, oneOf, anyOf
|
|
427
|
+
if (schema.allOf || schema.oneOf || schema.anyOf) {
|
|
428
|
+
return 'any'; // Could be expanded to intersection/union types
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return 'any';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get TypeScript type for request body
|
|
436
|
+
*/
|
|
437
|
+
private getRequestBodyType(requestBody: any): string {
|
|
438
|
+
if (!requestBody) return 'any';
|
|
439
|
+
|
|
440
|
+
// Handle content types
|
|
441
|
+
if (requestBody.content) {
|
|
442
|
+
// Prefer application/json
|
|
443
|
+
if (requestBody.content['application/json']?.schema) {
|
|
444
|
+
return this.getTypeFromSchema(requestBody.content['application/json'].schema);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Fall back to first available content type
|
|
448
|
+
const firstContentType = Object.keys(requestBody.content)[0];
|
|
449
|
+
if (requestBody.content[firstContentType]?.schema) {
|
|
450
|
+
return this.getTypeFromSchema(requestBody.content[firstContentType].schema);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return 'any';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Add request body properties directly to options array (flatten the body)
|
|
459
|
+
*/
|
|
460
|
+
private addRequestBodyProperties(requestBody: any, optionProps: string[]): void {
|
|
461
|
+
if (!requestBody) return;
|
|
462
|
+
|
|
463
|
+
let schema: any = null;
|
|
464
|
+
|
|
465
|
+
// Get the schema from the request body
|
|
466
|
+
if (requestBody.content) {
|
|
467
|
+
// Prefer application/json
|
|
468
|
+
if (requestBody.content['application/json']?.schema) {
|
|
469
|
+
schema = requestBody.content['application/json'].schema;
|
|
470
|
+
} else {
|
|
471
|
+
// Fall back to first available content type
|
|
472
|
+
const firstContentType = Object.keys(requestBody.content)[0];
|
|
473
|
+
if (requestBody.content[firstContentType]?.schema) {
|
|
474
|
+
schema = requestBody.content[firstContentType].schema;
|
|
423
475
|
}
|
|
424
476
|
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!schema) return;
|
|
480
|
+
|
|
481
|
+
// Handle $ref in schema
|
|
482
|
+
if (schema.$ref) {
|
|
483
|
+
const refType = this.resolveSchemaRef(schema.$ref);
|
|
484
|
+
const referencedSchema = this.spec.components?.schemas?.[refType];
|
|
485
|
+
if (referencedSchema && !('$ref' in referencedSchema)) {
|
|
486
|
+
schema = referencedSchema;
|
|
487
|
+
} else {
|
|
488
|
+
// If we can't resolve the reference, fall back to the original type
|
|
489
|
+
const bodyType = this.getRequestBodyType(requestBody);
|
|
490
|
+
optionProps.push(`body?: ${bodyType}`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
425
494
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
495
|
+
// If schema has properties, add them individually
|
|
496
|
+
if (schema.properties) {
|
|
497
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
498
|
+
const propType = this.getTypeFromSchema(propSchema as any);
|
|
499
|
+
const required = (schema.required && schema.required.includes(propName)) || requestBody.required;
|
|
500
|
+
const optional = required ? '' : '?';
|
|
501
|
+
|
|
502
|
+
// Add description as comment if available
|
|
503
|
+
const description = (propSchema as any)?.description;
|
|
504
|
+
if (description) {
|
|
505
|
+
// Clean up description for inline use
|
|
506
|
+
const cleanDesc = description.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
507
|
+
if (cleanDesc.length < 100) {
|
|
508
|
+
// Only add short descriptions inline
|
|
509
|
+
optionProps.push(`${propName}${optional}: ${propType} /** ${cleanDesc} */`);
|
|
510
|
+
} else {
|
|
511
|
+
optionProps.push(`${propName}${optional}: ${propType}`);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
optionProps.push(`${propName}${optional}: ${propType}`);
|
|
515
|
+
}
|
|
429
516
|
}
|
|
517
|
+
} else {
|
|
518
|
+
// If we can't extract individual properties, fall back to body wrapper
|
|
519
|
+
const bodyType = this.getRequestBodyType(requestBody);
|
|
520
|
+
optionProps.push(`body?: ${bodyType}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get TypeScript type for response
|
|
526
|
+
*/
|
|
527
|
+
private getResponseType(operation: OperationInfo): string {
|
|
528
|
+
if (!operation.responses) return 'any';
|
|
529
|
+
|
|
530
|
+
// Try success responses first (200, 201, etc.)
|
|
531
|
+
const successCodes = ['200', '201', '202', '204'];
|
|
532
|
+
for (const code of successCodes) {
|
|
533
|
+
if (operation.responses[code]) {
|
|
534
|
+
const response = operation.responses[code];
|
|
535
|
+
if (response.content) {
|
|
536
|
+
// Prefer application/json
|
|
537
|
+
if (response.content['application/json']?.schema) {
|
|
538
|
+
return this.getTypeFromSchema(response.content['application/json'].schema);
|
|
539
|
+
}
|
|
430
540
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
541
|
+
// Fall back to first available content type
|
|
542
|
+
const firstContentType = Object.keys(response.content)[0];
|
|
543
|
+
if (response.content[firstContentType]?.schema) {
|
|
544
|
+
return this.getTypeFromSchema(response.content[firstContentType].schema);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
434
547
|
}
|
|
435
548
|
}
|
|
436
549
|
|
|
@@ -438,62 +551,113 @@ export class OpenAPIToConnector {
|
|
|
438
551
|
}
|
|
439
552
|
|
|
440
553
|
/**
|
|
441
|
-
*
|
|
554
|
+
* Get TypeScript type for a parameter based on its schema
|
|
442
555
|
*/
|
|
443
|
-
private
|
|
444
|
-
|
|
556
|
+
private getParameterType(param: any): string {
|
|
557
|
+
if (param.schema) {
|
|
558
|
+
return this.getTypeFromSchema(param.schema);
|
|
559
|
+
}
|
|
560
|
+
return 'any';
|
|
561
|
+
}
|
|
445
562
|
|
|
446
|
-
|
|
447
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Generate method implementation code for controller methods with discrete path parameters
|
|
565
|
+
*/
|
|
566
|
+
private generateControllerMethodImplementation(operation: OperationInfo): string {
|
|
567
|
+
const lines: string[] = [];
|
|
568
|
+
const url = operation.path;
|
|
448
569
|
const pathParams: string[] = [];
|
|
449
570
|
const queryParams: string[] = [];
|
|
450
571
|
const hasBody = !!operation.requestBody;
|
|
451
572
|
|
|
452
|
-
//
|
|
573
|
+
// Check if method has any options (query params, body, or headers)
|
|
574
|
+
const hasOptions = queryParams.length > 0 || hasBody;
|
|
575
|
+
|
|
576
|
+
// Identify parameters
|
|
453
577
|
if (operation.parameters) {
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
queryParams.push(param.name);
|
|
460
|
-
}
|
|
578
|
+
operation.parameters.forEach((param: any) => {
|
|
579
|
+
if (param.in === 'path') {
|
|
580
|
+
pathParams.push(param.name);
|
|
581
|
+
} else if (param.in === 'query') {
|
|
582
|
+
queryParams.push(param.name);
|
|
461
583
|
}
|
|
462
|
-
}
|
|
584
|
+
});
|
|
463
585
|
}
|
|
464
586
|
|
|
465
|
-
//
|
|
466
|
-
const
|
|
587
|
+
// Update hasOptions after we know about query params
|
|
588
|
+
const actuallyHasOptions = queryParams.length > 0 || hasBody;
|
|
467
589
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
590
|
+
// Always extract path parameters as discrete parameters when they exist
|
|
591
|
+
if (pathParams.length > 0) {
|
|
592
|
+
// Handle path parameters as discrete function parameters
|
|
471
593
|
lines.push(` let url = '${url}';`);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
594
|
+
for (const paramName of pathParams) {
|
|
595
|
+
lines.push(` if (${paramName}) {`);
|
|
596
|
+
lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
|
|
597
|
+
lines.push(` }`);
|
|
598
|
+
}
|
|
475
599
|
lines.push('');
|
|
476
|
-
|
|
600
|
+
|
|
601
|
+
// Build request body by excluding query parameters and headers (only if we have options)
|
|
602
|
+
if (hasBody && actuallyHasOptions) {
|
|
603
|
+
const excludedParams = ['headers', ...queryParams];
|
|
604
|
+
const destructureList = excludedParams.join(', ');
|
|
605
|
+
lines.push(` const { ${destructureList}, ...bodyData } = options;`);
|
|
606
|
+
lines.push(` const requestBody = Object.keys(bodyData).length > 0 ? bodyData : undefined;`);
|
|
607
|
+
lines.push('');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Build fetch options
|
|
611
|
+
lines.push(` const fetchOptions: any = {`);
|
|
477
612
|
lines.push(` method: '${operation.method}',`);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
613
|
+
|
|
614
|
+
// Add query parameters
|
|
615
|
+
if (queryParams.length > 0) {
|
|
616
|
+
lines.push(` params: {},`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Add body
|
|
620
|
+
if (hasBody) {
|
|
621
|
+
if (actuallyHasOptions) {
|
|
622
|
+
lines.push(` body: requestBody,`);
|
|
623
|
+
} else {
|
|
624
|
+
lines.push(` body: undefined,`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Add headers only if we have options
|
|
629
|
+
if (actuallyHasOptions) {
|
|
630
|
+
lines.push(` headers: options?.headers,`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
lines.push(` };`);
|
|
483
634
|
lines.push('');
|
|
484
635
|
|
|
485
|
-
//
|
|
486
|
-
if (
|
|
487
|
-
lines.push(` //
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
lines.push(`
|
|
491
|
-
lines.push(` url = url.replace('{${paramName}}', options.${paramName});`);
|
|
636
|
+
// Add query parameters to options
|
|
637
|
+
if (queryParams.length > 0) {
|
|
638
|
+
lines.push(` // Add query parameters`);
|
|
639
|
+
for (const paramName of queryParams) {
|
|
640
|
+
lines.push(` if (options?.${paramName} !== undefined) {`);
|
|
641
|
+
lines.push(` fetchOptions.params.${paramName} = options.${paramName};`);
|
|
492
642
|
lines.push(` }`);
|
|
493
643
|
}
|
|
494
644
|
lines.push('');
|
|
495
|
-
}
|
|
496
|
-
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
// No path parameters - check if we have options
|
|
648
|
+
if (actuallyHasOptions) {
|
|
649
|
+
lines.push(` options = options || {};`);
|
|
650
|
+
lines.push('');
|
|
651
|
+
}
|
|
652
|
+
lines.push(` const url = '${url}';`);
|
|
653
|
+
lines.push('');
|
|
654
|
+
|
|
655
|
+
// Build request body by excluding query parameters and headers (only if we have options)
|
|
656
|
+
if (hasBody && actuallyHasOptions) {
|
|
657
|
+
const excludedParams = ['headers', ...queryParams];
|
|
658
|
+
const destructureList = excludedParams.join(', ');
|
|
659
|
+
lines.push(` const { ${destructureList}, ...bodyData } = options;`);
|
|
660
|
+
lines.push(` const requestBody = Object.keys(bodyData).length > 0 ? bodyData : undefined;`);
|
|
497
661
|
lines.push('');
|
|
498
662
|
}
|
|
499
663
|
|
|
@@ -506,13 +670,19 @@ export class OpenAPIToConnector {
|
|
|
506
670
|
lines.push(` params: {},`);
|
|
507
671
|
}
|
|
508
672
|
|
|
509
|
-
// Add body
|
|
673
|
+
// Add body
|
|
510
674
|
if (hasBody) {
|
|
511
|
-
|
|
675
|
+
if (actuallyHasOptions) {
|
|
676
|
+
lines.push(` body: requestBody,`);
|
|
677
|
+
} else {
|
|
678
|
+
lines.push(` body: undefined,`);
|
|
679
|
+
}
|
|
512
680
|
}
|
|
513
681
|
|
|
514
|
-
// Add headers if
|
|
515
|
-
|
|
682
|
+
// Add headers only if we have options
|
|
683
|
+
if (actuallyHasOptions) {
|
|
684
|
+
lines.push(` headers: options.headers,`);
|
|
685
|
+
}
|
|
516
686
|
|
|
517
687
|
lines.push(` };`);
|
|
518
688
|
lines.push('');
|
|
@@ -527,24 +697,604 @@ export class OpenAPIToConnector {
|
|
|
527
697
|
}
|
|
528
698
|
lines.push('');
|
|
529
699
|
}
|
|
700
|
+
}
|
|
701
|
+
// Make the API call
|
|
702
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
703
|
+
|
|
704
|
+
return lines.join('\n');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Generate method implementation for resource functions (using this.api instead of this.controller)
|
|
709
|
+
*/
|
|
710
|
+
private generateResourceFunctionImplementation(operation: OperationInfo): string {
|
|
711
|
+
const lines: string[] = [];
|
|
712
|
+
const url = operation.path;
|
|
713
|
+
const pathParams: string[] = [];
|
|
714
|
+
const queryParams: string[] = [];
|
|
715
|
+
const hasBody = !!operation.requestBody;
|
|
716
|
+
|
|
717
|
+
// Identify parameters
|
|
718
|
+
if (operation.parameters) {
|
|
719
|
+
operation.parameters.forEach((param: any) => {
|
|
720
|
+
if (param.in === 'path') {
|
|
721
|
+
pathParams.push(param.name);
|
|
722
|
+
} else if (param.in === 'query') {
|
|
723
|
+
queryParams.push(param.name);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Extract path parameters as discrete parameters when no query params or body
|
|
729
|
+
const isSimple = queryParams.length === 0 && !hasBody;
|
|
730
|
+
|
|
731
|
+
if (isSimple && pathParams.length > 0) {
|
|
732
|
+
// Handle path parameters as discrete function parameters
|
|
733
|
+
lines.push(` let url = '${url}';`);
|
|
734
|
+
for (const paramName of pathParams) {
|
|
735
|
+
lines.push(` if (${paramName}) {`);
|
|
736
|
+
lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
|
|
737
|
+
lines.push(` }`);
|
|
738
|
+
}
|
|
739
|
+
lines.push('');
|
|
740
|
+
lines.push(` return this.api.fetch(url, {`);
|
|
741
|
+
lines.push(` method: '${operation.method}',`);
|
|
742
|
+
lines.push(` });`);
|
|
743
|
+
} else {
|
|
744
|
+
// Options object pattern
|
|
745
|
+
lines.push(` options = options || {};`);
|
|
746
|
+
lines.push('');
|
|
747
|
+
|
|
748
|
+
// Replace path parameters - use discrete parameters, not options
|
|
749
|
+
if (pathParams.length > 0) {
|
|
750
|
+
lines.push(` // Build URL with path parameters`);
|
|
751
|
+
lines.push(` let url = '${url}';`);
|
|
752
|
+
for (const paramName of pathParams) {
|
|
753
|
+
lines.push(` if (${paramName}) {`);
|
|
754
|
+
lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
|
|
755
|
+
lines.push(` }`);
|
|
756
|
+
}
|
|
757
|
+
lines.push('');
|
|
758
|
+
} else {
|
|
759
|
+
lines.push(` const url = '${url}';`);
|
|
760
|
+
lines.push('');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Build request body by excluding query parameters and headers
|
|
764
|
+
if (hasBody) {
|
|
765
|
+
const excludedParams = ['headers', ...queryParams];
|
|
766
|
+
const destructureList = excludedParams.join(', ');
|
|
767
|
+
lines.push(` const { ${destructureList}, ...bodyData } = options;`);
|
|
768
|
+
lines.push(` const requestBody = Object.keys(bodyData).length > 0 ? bodyData : undefined;`);
|
|
769
|
+
lines.push('');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Build fetch options
|
|
773
|
+
lines.push(` const fetchOptions: any = {`);
|
|
774
|
+
lines.push(` method: '${operation.method}',`);
|
|
775
|
+
|
|
776
|
+
// Add query parameters
|
|
777
|
+
if (queryParams.length > 0) {
|
|
778
|
+
lines.push(` params: {},`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Add body
|
|
782
|
+
if (hasBody) {
|
|
783
|
+
lines.push(` body: requestBody,`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Add headers
|
|
787
|
+
lines.push(` headers: options.headers,`);
|
|
788
|
+
|
|
789
|
+
lines.push(` };`);
|
|
790
|
+
lines.push('');
|
|
791
|
+
|
|
792
|
+
// Add query parameters to options
|
|
793
|
+
if (queryParams.length > 0) {
|
|
794
|
+
lines.push(` // Add query parameters`);
|
|
795
|
+
for (const paramName of queryParams) {
|
|
796
|
+
lines.push(` if (options.${paramName} !== undefined) {`);
|
|
797
|
+
lines.push(` fetchOptions.params.${paramName} = options.${paramName};`);
|
|
798
|
+
lines.push(` }`);
|
|
799
|
+
}
|
|
800
|
+
lines.push('');
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Make the API call
|
|
804
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
805
|
+
|
|
806
|
+
return lines.join('\n');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Generate exposed resource methods for API introspection
|
|
811
|
+
*/
|
|
812
|
+
generateExposedResourceMethods(
|
|
813
|
+
resources: Array<{className: string; fileName: string}>,
|
|
814
|
+
resourceSpecs?: Array<{fileName: string; spec: OpenAPIV3.Document}>
|
|
815
|
+
): string {
|
|
816
|
+
const methods: string[] = [];
|
|
817
|
+
|
|
818
|
+
for (const resource of resources) {
|
|
819
|
+
const resourceName = resource.fileName;
|
|
820
|
+
|
|
821
|
+
// Find the corresponding spec for this resource
|
|
822
|
+
const resourceSpec = resourceSpecs?.find((rs) => rs.fileName === resourceName);
|
|
823
|
+
|
|
824
|
+
if (resourceSpec) {
|
|
825
|
+
// Create a temporary generator for this resource's spec
|
|
826
|
+
const resourceGenerator = new OpenAPIToConnector(resourceSpec.spec, resourceName);
|
|
827
|
+
const operations = resourceGenerator.extractOperations();
|
|
828
|
+
|
|
829
|
+
for (const operation of operations) {
|
|
830
|
+
const methodName = resourceGenerator.generateMethodName(operation);
|
|
831
|
+
const jsdoc = resourceGenerator.generateDetailedJSDoc(operation);
|
|
832
|
+
const signature = resourceGenerator.generateMethodSignature(operation);
|
|
833
|
+
|
|
834
|
+
// Generate the exposed method that delegates to the resource
|
|
835
|
+
const exposedMethodName = `${resourceName}${methodName.charAt(0).toUpperCase() + methodName.slice(1)}`;
|
|
836
|
+
|
|
837
|
+
// Generate parameter call based on operation details
|
|
838
|
+
const parameterCall = this.generateParameterCallForOperation(operation, signature);
|
|
839
|
+
|
|
840
|
+
methods.push(` /**
|
|
841
|
+
${jsdoc}
|
|
842
|
+
*/
|
|
843
|
+
async ${exposedMethodName}${signature} {
|
|
844
|
+
return this.${resourceName}.${methodName}(${parameterCall});
|
|
845
|
+
}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return methods.join('\n\n');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Generate parameter call for a specific operation based on path parameters
|
|
855
|
+
*/
|
|
856
|
+
private generateParameterCallForOperation(operation: OperationInfo, signature: string): string {
|
|
857
|
+
const pathParams: string[] = [];
|
|
858
|
+
const queryParams: string[] = [];
|
|
859
|
+
const hasBody = !!operation.requestBody;
|
|
860
|
+
|
|
861
|
+
// Identify path and query parameters
|
|
862
|
+
if (operation.parameters) {
|
|
863
|
+
for (const param of operation.parameters) {
|
|
864
|
+
if (typeof param === 'object' && 'name' in param && 'in' in param) {
|
|
865
|
+
if (param.in === 'path') {
|
|
866
|
+
pathParams.push(param.name);
|
|
867
|
+
} else if (param.in === 'query') {
|
|
868
|
+
queryParams.push(param.name);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Extract parameter names from controller signature
|
|
875
|
+
const paramMatch = signature.match(/\(([^)]+)\)/);
|
|
876
|
+
if (!paramMatch || paramMatch[1].trim() === '') return '';
|
|
877
|
+
|
|
878
|
+
const allParams = paramMatch[1]
|
|
879
|
+
.split(',')
|
|
880
|
+
.map((p) => {
|
|
881
|
+
const paramName = p.trim().split(':')[0].trim();
|
|
882
|
+
return paramName.replace(/[?]/g, '');
|
|
883
|
+
})
|
|
884
|
+
.filter((p) => p.length > 0);
|
|
885
|
+
|
|
886
|
+
// Check if signature actually has options parameter
|
|
887
|
+
const hasOptionsParam = allParams.includes('options');
|
|
888
|
+
|
|
889
|
+
// Always extract path parameters as discrete parameters when they exist
|
|
890
|
+
if (pathParams.length > 0) {
|
|
891
|
+
// Path parameters are discrete, options is the last parameter (if it exists)
|
|
892
|
+
const pathParamNames = pathParams;
|
|
893
|
+
if (hasOptionsParam) {
|
|
894
|
+
return [...pathParamNames, 'options'].join(', ');
|
|
895
|
+
} else {
|
|
896
|
+
return pathParamNames.join(', ');
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
// No path parameters, everything goes in options object (if options exists)
|
|
900
|
+
if (hasOptionsParam) {
|
|
901
|
+
return allParams[0] || 'options';
|
|
902
|
+
} else {
|
|
903
|
+
return '';
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Generate TypeScript interface from OpenAPI schema
|
|
910
|
+
*/
|
|
911
|
+
private generateInterfaceFromSchema(name: string, schema: any): string {
|
|
912
|
+
if (!schema) return '';
|
|
913
|
+
|
|
914
|
+
const sanitizedName = this.sanitizeTypeName(name);
|
|
915
|
+
const lines: string[] = [];
|
|
916
|
+
lines.push(`export interface ${sanitizedName} {`);
|
|
917
|
+
|
|
918
|
+
if (schema.properties) {
|
|
919
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
920
|
+
const propType = this.getTypeFromSchema(propSchema as any);
|
|
921
|
+
const required = schema.required && schema.required.includes(propName);
|
|
922
|
+
const optional = required ? '' : '?';
|
|
923
|
+
|
|
924
|
+
// Add description as comment if available
|
|
925
|
+
const description = (propSchema as any)?.description;
|
|
926
|
+
if (description) {
|
|
927
|
+
lines.push(` /** ${description} */`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
lines.push(` ${propName}${optional}: ${propType};`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Handle allOf, oneOf, anyOf
|
|
935
|
+
if (schema.allOf) {
|
|
936
|
+
lines.push(
|
|
937
|
+
` // Inherits from: ${schema.allOf.map((s: any) => (s.$ref ? this.resolveSchemaRef(s.$ref) : 'unknown')).join(', ')}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
lines.push('}');
|
|
942
|
+
return lines.join('\n');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Generate all TypeScript interfaces from OpenAPI components
|
|
947
|
+
*/
|
|
948
|
+
private generateAllInterfaces(): string {
|
|
949
|
+
if (!this.spec.components?.schemas) {
|
|
950
|
+
return '';
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const interfaces: string[] = [];
|
|
954
|
+
const usedTypes = this.collectUsedSchemaTypes();
|
|
955
|
+
|
|
956
|
+
for (const [schemaName, schema] of Object.entries(this.spec.components.schemas)) {
|
|
957
|
+
const sanitizedSchemaName = this.sanitizeTypeName(schemaName);
|
|
958
|
+
if (usedTypes.has(sanitizedSchemaName)) {
|
|
959
|
+
const interfaceCode = this.generateInterfaceFromSchema(schemaName, schema);
|
|
960
|
+
if (interfaceCode) {
|
|
961
|
+
interfaces.push(interfaceCode);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (interfaces.length === 0) {
|
|
967
|
+
return '';
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return `// Generated TypeScript interfaces from OpenAPI schemas\n\n${interfaces.join('\n\n')}\n`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Generate detailed JSDoc with schema field information
|
|
975
|
+
*/
|
|
976
|
+
private generateDetailedJSDoc(operation: OperationInfo): string {
|
|
977
|
+
const lines: string[] = [];
|
|
978
|
+
lines.push(` * ${operation.summary || operation.operationId || 'API Operation'}`);
|
|
979
|
+
|
|
980
|
+
if (operation.description) {
|
|
981
|
+
lines.push(' *');
|
|
982
|
+
// Split long descriptions into multiple lines
|
|
983
|
+
const descLines = operation.description.split('\n');
|
|
984
|
+
descLines.forEach((line) => {
|
|
985
|
+
lines.push(` * ${line}`);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
lines.push(' *');
|
|
990
|
+
|
|
991
|
+
// Document path parameters with details
|
|
992
|
+
const pathParams: any[] = [];
|
|
993
|
+
const queryParams: any[] = [];
|
|
994
|
+
|
|
995
|
+
if (operation.parameters) {
|
|
996
|
+
for (const param of operation.parameters) {
|
|
997
|
+
if (typeof param === 'object' && 'name' in param && 'in' in param) {
|
|
998
|
+
if (param.in === 'path') {
|
|
999
|
+
pathParams.push(param);
|
|
1000
|
+
} else if (param.in === 'query') {
|
|
1001
|
+
queryParams.push(param);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Document discrete path parameters
|
|
1008
|
+
pathParams.forEach((param) => {
|
|
1009
|
+
const paramType = this.getParameterType(param);
|
|
1010
|
+
const paramDesc = param.description || '';
|
|
1011
|
+
lines.push(` * @param {${paramType}} ${param.name} ${paramDesc}`);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// Document options parameter with detailed schema information
|
|
1015
|
+
if (queryParams.length > 0 || operation.requestBody) {
|
|
1016
|
+
lines.push(' * @param {Object} options - Request options');
|
|
530
1017
|
|
|
531
|
-
//
|
|
532
|
-
|
|
1018
|
+
// Document query parameters
|
|
1019
|
+
queryParams.forEach((param) => {
|
|
1020
|
+
const paramType = this.getParameterType(param);
|
|
1021
|
+
const paramDesc = param.description || '';
|
|
1022
|
+
const required = param.required ? '(required)' : '(optional)';
|
|
1023
|
+
lines.push(` * @param {${paramType}} options.${param.name} ${required} - ${paramDesc} [query]`);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// Document request body with detailed schema info (now flattened)
|
|
1027
|
+
if (operation.requestBody) {
|
|
1028
|
+
this.addFlattenedBodyDocumentation(lines, operation.requestBody);
|
|
1029
|
+
}
|
|
1030
|
+
} else if (pathParams.length > 0) {
|
|
1031
|
+
lines.push(` * @param {Object} options (optional) - Request options`);
|
|
533
1032
|
}
|
|
534
1033
|
|
|
1034
|
+
// Document response with detailed schema information
|
|
1035
|
+
lines.push(' *');
|
|
1036
|
+
const returnType = this.getResponseType(operation);
|
|
1037
|
+
lines.push(` * @returns {Promise<${returnType}>} ${operation.method.toUpperCase()} ${operation.path} response`);
|
|
1038
|
+
|
|
1039
|
+
// Add detailed schema information for the return type
|
|
1040
|
+
this.addSchemaDetails(lines, returnType, 'response');
|
|
1041
|
+
|
|
535
1042
|
return lines.join('\n');
|
|
536
1043
|
}
|
|
537
1044
|
|
|
538
1045
|
/**
|
|
539
|
-
*
|
|
1046
|
+
* Add flattened request body documentation to JSDoc
|
|
1047
|
+
*/
|
|
1048
|
+
private addFlattenedBodyDocumentation(lines: string[], requestBody: any): void {
|
|
1049
|
+
if (!requestBody) return;
|
|
1050
|
+
|
|
1051
|
+
let schema: any = null;
|
|
1052
|
+
|
|
1053
|
+
// Get the schema from the request body
|
|
1054
|
+
if (requestBody.content) {
|
|
1055
|
+
if (requestBody.content['application/json']?.schema) {
|
|
1056
|
+
schema = requestBody.content['application/json'].schema;
|
|
1057
|
+
} else {
|
|
1058
|
+
const firstContentType = Object.keys(requestBody.content)[0];
|
|
1059
|
+
if (requestBody.content[firstContentType]?.schema) {
|
|
1060
|
+
schema = requestBody.content[firstContentType].schema;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (!schema) return;
|
|
1066
|
+
|
|
1067
|
+
// Handle $ref in schema
|
|
1068
|
+
if (schema.$ref) {
|
|
1069
|
+
const refType = this.resolveSchemaRef(schema.$ref);
|
|
1070
|
+
const referencedSchema = this.spec.components?.schemas?.[refType];
|
|
1071
|
+
if (referencedSchema && !('$ref' in referencedSchema)) {
|
|
1072
|
+
schema = referencedSchema;
|
|
1073
|
+
} else {
|
|
1074
|
+
// Fallback to original format if we can't resolve
|
|
1075
|
+
const bodyType = this.getRequestBodyType(requestBody);
|
|
1076
|
+
const bodyDesc = requestBody.description || 'Request body';
|
|
1077
|
+
const required = requestBody.required ? '(required)' : '(optional)';
|
|
1078
|
+
lines.push(` * @param {${bodyType}} options.body ${required} - ${bodyDesc}`);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Document individual properties from the body schema
|
|
1084
|
+
if (schema.properties) {
|
|
1085
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
1086
|
+
const propType = this.getTypeFromSchema(propSchema as any);
|
|
1087
|
+
const propRequired = (schema.required && schema.required.includes(propName)) || requestBody.required;
|
|
1088
|
+
const requiredText = propRequired ? '(required)' : '(optional)';
|
|
1089
|
+
const propDesc = (propSchema as any)?.description || '';
|
|
1090
|
+
|
|
1091
|
+
lines.push(` * @param {${propType}} options.${propName} ${requiredText} - ${propDesc} [body property]`);
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
// Fallback to original format if no properties
|
|
1095
|
+
const bodyType = this.getRequestBodyType(requestBody);
|
|
1096
|
+
const bodyDesc = requestBody.description || 'Request body';
|
|
1097
|
+
const required = requestBody.required ? '(required)' : '(optional)';
|
|
1098
|
+
lines.push(` * @param {${bodyType}} options.body ${required} - ${bodyDesc}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Add detailed schema field information to JSDoc
|
|
1104
|
+
*/
|
|
1105
|
+
private addSchemaDetails(lines: string[], typeName: string, context: string): void {
|
|
1106
|
+
// Remove array notation to get base type
|
|
1107
|
+
const baseType = typeName.replace(/\[\]$/, '');
|
|
1108
|
+
const isArray = typeName.endsWith('[]');
|
|
1109
|
+
|
|
1110
|
+
if (['string', 'number', 'boolean', 'any'].includes(baseType)) {
|
|
1111
|
+
return; // Skip primitive types
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const schema = this.spec.components?.schemas?.[baseType];
|
|
1115
|
+
if (!schema) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Type guard to check if schema has properties (is not a ReferenceObject)
|
|
1120
|
+
if ('$ref' in schema) {
|
|
1121
|
+
return; // Skip reference objects
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (!schema.properties) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
lines.push(' *');
|
|
1129
|
+
lines.push(` * ${context}${isArray ? '[]' : ''} fields:`);
|
|
1130
|
+
|
|
1131
|
+
const maxFields = 10; // Limit to avoid too much clutter
|
|
1132
|
+
const properties = Object.entries(schema.properties).slice(0, maxFields);
|
|
1133
|
+
|
|
1134
|
+
for (const [propName, propSchema] of properties) {
|
|
1135
|
+
const propType = this.getTypeFromSchema(propSchema as any);
|
|
1136
|
+
const required = schema.required && schema.required.includes(propName);
|
|
1137
|
+
const requiredText = required ? '' : '?';
|
|
1138
|
+
const description = (propSchema as any)?.description;
|
|
1139
|
+
|
|
1140
|
+
if (description) {
|
|
1141
|
+
lines.push(` * - ${propName}${requiredText}: ${propType} - ${description}`);
|
|
1142
|
+
} else {
|
|
1143
|
+
lines.push(` * - ${propName}${requiredText}: ${propType}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const totalFields = Object.keys(schema.properties).length;
|
|
1148
|
+
if (totalFields > maxFields) {
|
|
1149
|
+
lines.push(` * - ... and ${totalFields - maxFields} more fields`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Collect all schema types used in operations (including nested references)
|
|
1155
|
+
*/
|
|
1156
|
+
private collectUsedSchemaTypes(): Set<string> {
|
|
1157
|
+
const usedTypes = new Set<string>();
|
|
1158
|
+
const visitedSchemas = new Set<string>(); // Track visited schemas to prevent infinite recursion
|
|
1159
|
+
const operations = this.extractOperations();
|
|
1160
|
+
|
|
1161
|
+
for (const operation of operations) {
|
|
1162
|
+
// Collect from request body schemas
|
|
1163
|
+
if (operation.requestBody) {
|
|
1164
|
+
this.collectTypesFromRequestBody(operation.requestBody, usedTypes, visitedSchemas);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Collect from response schemas
|
|
1168
|
+
if (operation.responses) {
|
|
1169
|
+
this.collectTypesFromResponses(operation.responses, usedTypes, visitedSchemas);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Collect from parameter schemas
|
|
1173
|
+
if (operation.parameters) {
|
|
1174
|
+
for (const param of operation.parameters) {
|
|
1175
|
+
if (typeof param === 'object' && 'schema' in param && param.schema) {
|
|
1176
|
+
this.collectTypesFromSchema(param.schema, usedTypes, visitedSchemas);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return usedTypes;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Collect types from request body schema
|
|
1187
|
+
*/
|
|
1188
|
+
private collectTypesFromRequestBody(requestBody: any, usedTypes: Set<string>, visitedSchemas: Set<string>): void {
|
|
1189
|
+
if (!requestBody || !requestBody.content) return;
|
|
1190
|
+
|
|
1191
|
+
// Check all content types
|
|
1192
|
+
for (const contentType of Object.keys(requestBody.content)) {
|
|
1193
|
+
const content = requestBody.content[contentType];
|
|
1194
|
+
if (content.schema) {
|
|
1195
|
+
this.collectTypesFromSchema(content.schema, usedTypes, visitedSchemas);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Collect types from response schemas
|
|
1202
|
+
*/
|
|
1203
|
+
private collectTypesFromResponses(responses: any, usedTypes: Set<string>, visitedSchemas: Set<string>): void {
|
|
1204
|
+
if (!responses) return;
|
|
1205
|
+
|
|
1206
|
+
// Check success responses
|
|
1207
|
+
const successCodes = ['200', '201', '202', '204'];
|
|
1208
|
+
for (const code of successCodes) {
|
|
1209
|
+
const response = responses[code];
|
|
1210
|
+
if (response && response.content) {
|
|
1211
|
+
for (const contentType of Object.keys(response.content)) {
|
|
1212
|
+
const content = response.content[contentType];
|
|
1213
|
+
if (content.schema) {
|
|
1214
|
+
this.collectTypesFromSchema(content.schema, usedTypes, visitedSchemas);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Collect types from a schema object
|
|
1223
|
+
*/
|
|
1224
|
+
private collectTypesFromSchema(schema: any, usedTypes: Set<string>, visitedSchemas: Set<string>): void {
|
|
1225
|
+
if (!schema) return;
|
|
1226
|
+
|
|
1227
|
+
// Handle $ref
|
|
1228
|
+
if (schema.$ref) {
|
|
1229
|
+
const parts = schema.$ref.split('/');
|
|
1230
|
+
const originalSchemaName = parts[parts.length - 1];
|
|
1231
|
+
const sanitizedRefType = this.resolveSchemaRef(schema.$ref);
|
|
1232
|
+
|
|
1233
|
+
if (sanitizedRefType !== 'any' && !['string', 'number', 'boolean'].includes(sanitizedRefType)) {
|
|
1234
|
+
// Add the sanitized type name to used types
|
|
1235
|
+
usedTypes.add(sanitizedRefType);
|
|
1236
|
+
|
|
1237
|
+
// Only recurse if we haven't visited this schema before (use original name for lookup)
|
|
1238
|
+
if (!visitedSchemas.has(originalSchemaName)) {
|
|
1239
|
+
visitedSchemas.add(originalSchemaName);
|
|
1240
|
+
const referencedSchema = this.spec.components?.schemas?.[originalSchemaName];
|
|
1241
|
+
if (referencedSchema) {
|
|
1242
|
+
this.collectTypesFromSchema(referencedSchema, usedTypes, visitedSchemas);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Handle arrays
|
|
1250
|
+
if (schema.type === 'array' && schema.items) {
|
|
1251
|
+
this.collectTypesFromSchema(schema.items, usedTypes, visitedSchemas);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Handle objects with properties
|
|
1256
|
+
if (schema.type === 'object' && schema.properties) {
|
|
1257
|
+
for (const propSchema of Object.values(schema.properties)) {
|
|
1258
|
+
this.collectTypesFromSchema(propSchema, usedTypes, visitedSchemas);
|
|
1259
|
+
}
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Handle allOf, oneOf, anyOf
|
|
1264
|
+
if (schema.allOf) {
|
|
1265
|
+
for (const subSchema of schema.allOf) {
|
|
1266
|
+
this.collectTypesFromSchema(subSchema, usedTypes, visitedSchemas);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (schema.oneOf) {
|
|
1271
|
+
for (const subSchema of schema.oneOf) {
|
|
1272
|
+
this.collectTypesFromSchema(subSchema, usedTypes, visitedSchemas);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (schema.anyOf) {
|
|
1277
|
+
for (const subSchema of schema.anyOf) {
|
|
1278
|
+
this.collectTypesFromSchema(subSchema, usedTypes, visitedSchemas);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Generate type imports for used schema types
|
|
540
1285
|
*/
|
|
541
|
-
private
|
|
542
|
-
|
|
543
|
-
|
|
1286
|
+
private generateTypeImports(): string {
|
|
1287
|
+
const usedTypes = this.collectUsedSchemaTypes();
|
|
1288
|
+
if (usedTypes.size === 0) {
|
|
1289
|
+
return '';
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const typeList = Array.from(usedTypes).sort().join(', ');
|
|
1293
|
+
return `// Type imports (you may need to adjust the import path)\n// import type { ${typeList} } from './types';\n\n`;
|
|
544
1294
|
}
|
|
545
1295
|
|
|
546
1296
|
/**
|
|
547
|
-
* Generate a resource
|
|
1297
|
+
* Generate a resource file with exported functions (new pattern for proper introspection)
|
|
548
1298
|
*/
|
|
549
1299
|
generateResourceClass(className: string): string {
|
|
550
1300
|
const operations = this.extractOperations();
|
|
@@ -553,56 +1303,54 @@ export class OpenAPIToConnector {
|
|
|
553
1303
|
throw new Error('No operations found in OpenAPI specification');
|
|
554
1304
|
}
|
|
555
1305
|
|
|
556
|
-
const
|
|
1306
|
+
const resourceName = className.replace('Resource', '').toLowerCase();
|
|
1307
|
+
|
|
1308
|
+
const functions = operations
|
|
557
1309
|
.map((operation) => {
|
|
558
1310
|
const methodName = this.generateMethodName(operation);
|
|
559
|
-
const jsdoc = this.
|
|
1311
|
+
const jsdoc = this.generateDetailedJSDoc(operation);
|
|
560
1312
|
const signature = this.generateMethodSignature(operation);
|
|
561
|
-
const implementation = this.
|
|
1313
|
+
const implementation = this.generateResourceFunctionImplementation(operation);
|
|
562
1314
|
|
|
563
|
-
return
|
|
1315
|
+
return `/**\n${jsdoc}\n */\nexport function ${methodName}(this: any, ${signature.replace('(', '').replace(')', '')}) {\n${implementation}\n}`;
|
|
564
1316
|
})
|
|
565
1317
|
.join('\n\n');
|
|
566
1318
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
export default class ${className} {
|
|
570
|
-
private controller: AbstractController;
|
|
1319
|
+
// Generate actual TypeScript interfaces
|
|
1320
|
+
const interfaces = this.generateAllInterfaces();
|
|
571
1321
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
1322
|
+
return `// ${className} resource functions
|
|
1323
|
+
// These functions will be bound to the controller instance and accessible as ${resourceName}.method()
|
|
575
1324
|
|
|
576
|
-
|
|
577
|
-
return this.controller['api'];
|
|
578
|
-
}
|
|
1325
|
+
${interfaces}
|
|
579
1326
|
|
|
580
|
-
${
|
|
581
|
-
}`;
|
|
1327
|
+
${functions}`;
|
|
582
1328
|
}
|
|
583
1329
|
|
|
584
1330
|
/**
|
|
585
|
-
* Generate a main controller that composes multiple resources
|
|
1331
|
+
* Generate a main controller that composes multiple resources using function binding
|
|
586
1332
|
*/
|
|
587
|
-
generateMainController(
|
|
1333
|
+
generateMainController(
|
|
1334
|
+
resources: Array<{className: string; fileName: string}>,
|
|
1335
|
+
resourceSpecs?: Array<{fileName: string; spec: OpenAPIV3.Document}>
|
|
1336
|
+
): string {
|
|
588
1337
|
// Get base URL from servers if available
|
|
589
|
-
const baseUrl =
|
|
1338
|
+
const baseUrl =
|
|
1339
|
+
this.spec.servers && this.spec.servers.length > 0 ? this.spec.servers[0].url : 'https://api.example.com';
|
|
590
1340
|
|
|
591
1341
|
const imports = resources
|
|
592
|
-
.map((resource) => `import ${resource.
|
|
1342
|
+
.map((resource) => `import * as ${resource.fileName}Functions from '../resources/${resource.fileName}.mjs';`)
|
|
593
1343
|
.join('\n');
|
|
594
1344
|
|
|
595
|
-
const properties = resources
|
|
596
|
-
.map((resource) => ` ${resource.className.toLowerCase().replace('resource', '')}!: ${resource.className};`)
|
|
597
|
-
.join('\n');
|
|
1345
|
+
const properties = resources.map((resource) => ` ${resource.fileName}: any = {};`).join('\n');
|
|
598
1346
|
|
|
599
|
-
const
|
|
600
|
-
.map(
|
|
601
|
-
(resource) =>
|
|
602
|
-
` this.${resource.className.toLowerCase().replace('resource', '')} = new ${resource.className}(this);`
|
|
603
|
-
)
|
|
1347
|
+
const bindings = resources
|
|
1348
|
+
.map((resource) => ` this.bindResourceFunctions('${resource.fileName}', ${resource.fileName}Functions);`)
|
|
604
1349
|
.join('\n');
|
|
605
1350
|
|
|
1351
|
+
// Generate exposed methods for each resource to enable API introspection
|
|
1352
|
+
const exposedMethods = this.generateExposedResourceMethods(resources, resourceSpecs);
|
|
1353
|
+
|
|
606
1354
|
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
|
607
1355
|
${imports}
|
|
608
1356
|
|
|
@@ -612,18 +1360,45 @@ ${properties}
|
|
|
612
1360
|
private api: any;
|
|
613
1361
|
|
|
614
1362
|
protected async start(): Promise<void> {
|
|
1363
|
+
const config = this.config;
|
|
1364
|
+
|
|
615
1365
|
this.api = this.getClient({
|
|
616
1366
|
baseUrl: '${baseUrl}',
|
|
1367
|
+
customize(request) {
|
|
1368
|
+
request.headers ||= {};
|
|
1369
|
+
// Add authentication headers based on your API requirements
|
|
1370
|
+
// Example: request.headers["Authorization"] = \`Bearer \${config.apiToken}\`;
|
|
1371
|
+
},
|
|
617
1372
|
});
|
|
618
1373
|
|
|
619
|
-
//
|
|
620
|
-
|
|
1374
|
+
// Bind resource functions to this controller context
|
|
1375
|
+
// This allows using this.resourceName.method() syntax
|
|
1376
|
+
${bindings}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private bindResourceFunctions(resourceName: string, functions: any) {
|
|
1380
|
+
for (const [functionName, func] of Object.entries(functions)) {
|
|
1381
|
+
if (typeof func === 'function') {
|
|
1382
|
+
this[resourceName][functionName] = func.bind(this);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
621
1385
|
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Generic API request method
|
|
1389
|
+
* @param url - API endpoint
|
|
1390
|
+
* @param options - Request options
|
|
1391
|
+
*/
|
|
1392
|
+
async request({ url, options }: { url: string; options?: any }) {
|
|
1393
|
+
return this.api.fetch(url, options);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
${exposedMethods}
|
|
622
1397
|
}`;
|
|
623
1398
|
}
|
|
624
1399
|
|
|
625
1400
|
/**
|
|
626
|
-
* Generate the connector controller code
|
|
1401
|
+
* Generate the connector controller code with improved pattern
|
|
627
1402
|
*/
|
|
628
1403
|
generateController(): string {
|
|
629
1404
|
const operations = this.extractOperations();
|
|
@@ -635,28 +1410,42 @@ ${initializations}
|
|
|
635
1410
|
const methods = operations
|
|
636
1411
|
.map((operation) => {
|
|
637
1412
|
const methodName = this.generateMethodName(operation);
|
|
638
|
-
const jsdoc = this.
|
|
1413
|
+
const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
|
|
639
1414
|
const signature = this.generateMethodSignature(operation);
|
|
640
|
-
const implementation = this.
|
|
1415
|
+
const implementation = this.generateControllerMethodImplementation(operation); // Use improved implementation
|
|
641
1416
|
|
|
642
1417
|
return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
|
|
643
1418
|
})
|
|
644
1419
|
.join('\n\n');
|
|
645
1420
|
|
|
646
1421
|
// Get base URL from servers if available
|
|
647
|
-
const baseUrl =
|
|
1422
|
+
const baseUrl =
|
|
1423
|
+
this.spec.servers && this.spec.servers.length > 0 ? this.spec.servers[0].url : 'https://api.example.com';
|
|
1424
|
+
|
|
1425
|
+
// Generate TypeScript interfaces
|
|
1426
|
+
const interfaces = this.generateAllInterfaces();
|
|
1427
|
+
|
|
1428
|
+
// Generate type imports
|
|
1429
|
+
const typeImports = this.generateTypeImports();
|
|
648
1430
|
|
|
649
1431
|
const startMethod = ` private api: any;
|
|
650
1432
|
|
|
651
1433
|
protected async start(): Promise<void> {
|
|
1434
|
+
const config = this.config;
|
|
1435
|
+
|
|
652
1436
|
this.api = this.getClient({
|
|
653
1437
|
baseUrl: '${baseUrl}',
|
|
1438
|
+
customize(request) {
|
|
1439
|
+
request.headers ||= {};
|
|
1440
|
+
// Add authentication headers based on your API requirements
|
|
1441
|
+
// Example: request.headers["Authorization"] = \`Bearer \${config.apiToken}\`;
|
|
1442
|
+
},
|
|
654
1443
|
});
|
|
655
1444
|
}`;
|
|
656
1445
|
|
|
657
1446
|
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
|
658
1447
|
|
|
659
|
-
export default class Controller extends AbstractController {
|
|
1448
|
+
${typeImports}${interfaces}export default class Controller extends AbstractController {
|
|
660
1449
|
|
|
661
1450
|
${startMethod}
|
|
662
1451
|
|