@aloma.io/integration-sdk 3.8.55 → 3.8.56

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.
@@ -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
- // If there are no query params, no body, and only path params, use simple signature
307
- if (queryParams.length === 0 && !hasBody && pathParams.length <= 1) {
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
- params.push(`options?: {headers?: {[key: string]: any}}`);
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
- optionProps.push('body?: any');
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
- const required = hasRequiredParams ? '' : '?';
397
- return `(options${required}: {${optionProps.join(', ')}})`;
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
- * Get TypeScript type for a parameter based on its schema
330
+ * Resolve a schema reference to a TypeScript type name
402
331
  */
403
- private getParameterType(param: any): string {
404
- if (param.schema) {
405
- const schema = param.schema;
406
-
407
- // Handle different schema types
408
- if (schema.type) {
409
- switch (schema.type) {
410
- case 'string':
411
- return 'string';
412
- case 'integer':
413
- case 'number':
414
- return 'number';
415
- case 'boolean':
416
- return 'boolean';
417
- case 'array':
418
- return 'any[]';
419
- case 'object':
420
- return 'any';
421
- default:
422
- return 'any';
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
- // Handle enum
427
- if (schema.enum) {
428
- return 'string';
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
- // Handle $ref
432
- if (schema.$ref) {
433
- return 'any';
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
- * Generate method implementation code
554
+ * Get TypeScript type for a parameter based on its schema
442
555
  */
443
- private generateMethodImplementation(operation: OperationInfo): string {
444
- const lines: string[] = [];
556
+ private getParameterType(param: any): string {
557
+ if (param.schema) {
558
+ return this.getTypeFromSchema(param.schema);
559
+ }
560
+ return 'any';
561
+ }
445
562
 
446
- // Build URL with path parameters
447
- let url = operation.path;
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
- // Identify path and query parameters
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
- for (const param of operation.parameters) {
455
- if (typeof param === 'object' && 'name' in param && 'in' in param) {
456
- if (param.in === 'path') {
457
- pathParams.push(param.name);
458
- } else if (param.in === 'query') {
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
- // Check if using simple signature (single path param, no query/body)
466
- const useSimpleSignature = queryParams.length === 0 && !hasBody && pathParams.length <= 1;
587
+ // Update hasOptions after we know about query params
588
+ const actuallyHasOptions = queryParams.length > 0 || hasBody;
467
589
 
468
- if (useSimpleSignature && pathParams.length === 1) {
469
- // Simple signature: (pathParam: string, options?: {headers?: ...})
470
- const paramName = pathParams[0];
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
- lines.push(` if (${paramName}) {`);
473
- lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
474
- lines.push(` }`);
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
- lines.push(` return this.api.fetch(url, {`);
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
- lines.push(` headers: options?.headers,`);
479
- lines.push(` });`);
480
- } else {
481
- // Options object pattern
482
- lines.push(` options = options || {};`);
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
- // Replace path parameters
486
- if (pathParams.length > 0) {
487
- lines.push(` // Build URL with path parameters`);
488
- lines.push(` let url = '${url}';`);
489
- for (const paramName of pathParams) {
490
- lines.push(` if (options.${paramName}) {`);
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
- } else {
496
- lines.push(` const url = '${url}';`);
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 if present
673
+ // Add body
510
674
  if (hasBody) {
511
- lines.push(` body: options.body,`);
675
+ if (actuallyHasOptions) {
676
+ lines.push(` body: requestBody,`);
677
+ } else {
678
+ lines.push(` body: undefined,`);
679
+ }
512
680
  }
513
681
 
514
- // Add headers if present
515
- lines.push(` headers: options.headers,`);
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
- // Make the API call
532
- lines.push(` return this.api.fetch(url, fetchOptions);`);
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
- * Generate proper import paths with .mjs extensions for TypeScript module resolution
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 generateImportPath(relativePath: string): string {
542
- // For resource classes, we need to reference the compiled .mjs files
543
- return relativePath.endsWith('.mjs') ? relativePath : `${relativePath}.mjs`;
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 class (does NOT extend AbstractController, receives controller reference)
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 methods = operations
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.generateJSDoc(operation);
1311
+ const jsdoc = this.generateDetailedJSDoc(operation);
560
1312
  const signature = this.generateMethodSignature(operation);
561
- const implementation = this.generateMethodImplementation(operation);
1313
+ const implementation = this.generateResourceFunctionImplementation(operation);
562
1314
 
563
- return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
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
- return `import {AbstractController} from '@aloma.io/integration-sdk';
568
-
569
- export default class ${className} {
570
- private controller: AbstractController;
1319
+ // Generate actual TypeScript interfaces
1320
+ const interfaces = this.generateAllInterfaces();
571
1321
 
572
- constructor(controller: AbstractController) {
573
- this.controller = controller;
574
- }
1322
+ return `// ${className} resource functions
1323
+ // These functions will be bound to the controller instance and accessible as ${resourceName}.method()
575
1324
 
576
- private get api() {
577
- return this.controller['api'];
578
- }
1325
+ ${interfaces}
579
1326
 
580
- ${methods}
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(resources: Array<{className: string; fileName: string}>): string {
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 = this.spec.servers && this.spec.servers.length > 0 ? this.spec.servers[0].url : 'API_BASE_URL';
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.className} from '../resources/${resource.fileName}.mjs';`)
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 initializations = resources
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
- // Initialize each resource - they receive 'this' controller reference
620
- ${initializations}
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.generateJSDoc(operation);
1413
+ const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
639
1414
  const signature = this.generateMethodSignature(operation);
640
- const implementation = this.generateMethodImplementation(operation);
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 = this.spec.servers && this.spec.servers.length > 0 ? this.spec.servers[0].url : 'API_BASE_URL';
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