@aloma.io/integration-sdk 3.8.54 → 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.
Files changed (35) hide show
  1. package/MULTI_RESOURCE_GUIDE.md +24 -21
  2. package/OPENAPI_TO_CONNECTOR.md +146 -16
  3. package/README.md +62 -10
  4. package/build/cli.mjs +122 -33
  5. package/build/openapi-to-connector.d.mts +92 -11
  6. package/build/openapi-to-connector.mjs +968 -168
  7. package/package.json +3 -1
  8. package/src/cli.mts +140 -37
  9. package/src/openapi-to-connector.mts +1092 -176
  10. package/test/scenarios/README.md +148 -0
  11. package/test/scenarios/complex/expected/controller.mts +271 -0
  12. package/test/scenarios/complex/expected/orders-resource.mts +264 -0
  13. package/test/scenarios/complex/expected/products-resource.mts +239 -0
  14. package/test/scenarios/complex/specs/orders.json +362 -0
  15. package/test/scenarios/complex/specs/products.json +308 -0
  16. package/test/scenarios/simple/expected-controller.mts +60 -0
  17. package/test/scenarios/simple/simple-api.json +39 -0
  18. package/test/scenarios.test.mts +286 -0
  19. package/test/verify-scenarios.mjs +298 -0
  20. package/examples/api-without-servers.json +0 -32
  21. package/examples/companies-resource-class.mts +0 -310
  22. package/examples/companies-resource.mts +0 -310
  23. package/examples/complete-example.sh +0 -116
  24. package/examples/create-hubspot-connector.sh +0 -33
  25. package/examples/generate-connector.sh +0 -35
  26. package/examples/generated-controller.mts +0 -81
  27. package/examples/hubspot-companies.json +0 -1889
  28. package/examples/hubspot-contacts.json +0 -1919
  29. package/examples/hubspot-controller-individual-params.mts +0 -323
  30. package/examples/hubspot-controller-with-implementation.mts +0 -315
  31. package/examples/hubspot-controller.mts +0 -192
  32. package/examples/hubspot-lists.json +0 -5525
  33. package/examples/main-controller-with-resources.mts +0 -35
  34. package/examples/stripe.json +0 -182829
  35. package/examples/utility-click.json +0 -8992
@@ -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';
@@ -174,199 +173,491 @@ export class OpenAPIToConnector {
174
173
  }
175
174
 
176
175
  /**
177
- * Generate JSDoc comment for an operation
176
+ * Get the number of operations in the OpenAPI spec
178
177
  */
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
- }
178
+ getOperationsCount(): number {
179
+ return this.extractOperations().length;
180
+ }
188
181
 
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
- }
182
+ /**
183
+ * Generate method signature with options object
184
+ */
185
+ private generateMethodSignature(operation: OperationInfo): string {
186
+ const pathParams: Array<{name: string; required: boolean; type: string}> = [];
187
+ const queryParams: Array<{name: string; required: boolean; type: string}> = [];
188
+ const hasBody = !!operation.requestBody;
199
189
 
200
- // Identify path and query parameters
190
+ // Identify path and query parameters with their types and required status
201
191
  if (operation.parameters) {
202
192
  for (const param of operation.parameters) {
203
193
  if (typeof param === 'object' && 'name' in param && 'in' in param) {
194
+ const paramInfo = {
195
+ name: param.name,
196
+ required: param.required || false,
197
+ type: this.getParameterType(param),
198
+ };
199
+
204
200
  if (param.in === 'path') {
205
- pathParams.push(param);
201
+ pathParams.push(paramInfo);
206
202
  } else if (param.in === 'query') {
207
- queryParams.push(param);
203
+ queryParams.push(paramInfo);
208
204
  }
209
205
  }
210
206
  }
211
207
  }
212
208
 
213
- // Check if using simple signature
214
- const useSimpleSignature = queryParams.length === 0 && !hasBody && pathParams.length <= 1;
209
+ // Always extract path parameters as discrete parameters when they exist
210
+ if (pathParams.length > 0) {
211
+ const params: string[] = [];
212
+ for (const paramInfo of pathParams) {
213
+ params.push(`${paramInfo.name}: ${paramInfo.type}`);
214
+ }
215
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`);
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
+
240
+ return `(${params.join(', ')})`;
241
+ }
242
+
243
+ // Check if there are any required parameters
244
+ const hasRequiredParams =
245
+ pathParams.some((p) => p.required) ||
246
+ queryParams.some((p) => p.required) ||
247
+ (hasBody && operation.requestBody?.required);
248
+
249
+ // Build detailed options object with proper types
250
+ // Group nested properties into objects (e.g., PrimaryContact.FirstName -> PrimaryContact: {FirstName: string})
251
+ const nestedObjects: Map<string, Array<{name: string; type: string; required: boolean}>> = new Map();
252
+ const flatProps: Array<{name: string; type: string; required: boolean}> = [];
253
+
254
+ // Process all parameters (path + query)
255
+ const allParams = [...pathParams, ...queryParams];
256
+
257
+ for (const paramInfo of allParams) {
258
+ if (paramInfo.name.includes('.')) {
259
+ // This is a nested property like PrimaryContact.FirstName
260
+ const parts = paramInfo.name.split('.');
261
+ const objectName = parts[0];
262
+ const propertyName = parts.slice(1).join('.');
263
+
264
+ if (!nestedObjects.has(objectName)) {
265
+ nestedObjects.set(objectName, []);
266
+ }
267
+ nestedObjects.get(objectName)!.push({
268
+ name: propertyName,
269
+ type: paramInfo.type,
270
+ required: paramInfo.required,
271
+ });
272
+ } else {
273
+ // This is a flat property
274
+ flatProps.push({
275
+ name: paramInfo.name,
276
+ type: paramInfo.type,
277
+ required: paramInfo.required,
278
+ });
279
+ }
280
+ }
281
+
282
+ // Build the options properties array
283
+ const optionProps: string[] = [];
284
+
285
+ // Add flat properties
286
+ for (const prop of flatProps) {
287
+ const optional = prop.required ? '' : '?';
288
+ optionProps.push(`${prop.name}${optional}: ${prop.type}`);
289
+ }
290
+
291
+ // Add nested objects
292
+ for (const [objectName, properties] of nestedObjects) {
293
+ const nestedProps = properties
294
+ .map((p) => {
295
+ const optional = p.required ? '' : '?';
296
+ return `${p.name}${optional}: ${p.type}`;
297
+ })
298
+ .join(', ');
299
+
300
+ // Check if all properties are optional
301
+ const allOptional = properties.every((p) => !p.required);
302
+ const optional = allOptional ? '?' : '';
303
+
304
+ optionProps.push(`${objectName}${optional}: {${nestedProps}}`);
305
+ }
306
+
307
+ // Add request body properties directly (flattened)
308
+ if (hasBody) {
309
+ this.addRequestBodyProperties(operation.requestBody, optionProps);
310
+ }
311
+
312
+ // If there are too many parameters, use simplified signature to avoid parsing issues
313
+ // Also check if any parameter name is too long (over 100 chars) which can cause issues
314
+ const hasLongParamNames = optionProps.some((prop) => prop.length > 100);
315
+ if (optionProps.length > 15 || hasLongParamNames) {
316
+ const required = hasRequiredParams ? '' : '?';
317
+ return `(options${required}: {[key: string]: any})`;
318
+ }
319
+
320
+ // Only add options if there are actual options
321
+ if (optionProps.length > 0) {
322
+ const required = hasRequiredParams ? '' : '?';
323
+ return `(options${required}: {${optionProps.join(', ')}})`;
225
324
  } else {
226
- // Options object documentation
227
- lines.push(' *');
228
- lines.push(` * @param {Object} options (optional) - Request options`);
325
+ return '()';
326
+ }
327
+ }
229
328
 
230
- // Document path parameters
231
- for (const param of pathParams) {
232
- const paramType = param.schema?.type || 'string';
233
- const paramDesc = param.description || '';
234
- const paramRequired = param.required ? '(required)' : '(optional)';
235
- lines.push(` * @param {${paramType}} options.${param.name} ${paramRequired} - ${paramDesc} [path]`);
329
+ /**
330
+ * Resolve a schema reference to a TypeScript type name
331
+ */
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}[]`;
236
382
  }
383
+ return 'any[]';
384
+ }
237
385
 
238
- // Document query parameters
239
- for (const param of queryParams) {
240
- const paramType = param.schema?.type || 'any';
241
- const paramDesc = param.description || '';
242
- const paramRequired = param.required ? '(required)' : '(optional)';
243
- lines.push(` * @param {${paramType}} options.${param.name} ${paramRequired} - ${paramDesc} [query]`);
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('; ')}}`;
244
398
  }
245
399
 
246
- // Document request body
247
- if (operation.requestBody) {
248
- const bodyDesc = operation.requestBody.description || 'Request body';
249
- const required = operation.requestBody.required ? '(required)' : '(optional)';
250
- lines.push(` * @param {Object} options.body ${required} - ${bodyDesc}`);
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';
251
418
  }
419
+ }
252
420
 
253
- // Document headers
254
- lines.push(` * @param {Object} options.headers (optional) - Custom headers to include in the request`);
421
+ // Handle enum
422
+ if (schema.enum) {
423
+ return 'string'; // Could be expanded to union types
255
424
  }
256
425
 
257
- // Document response
258
- lines.push(' *');
259
- lines.push(` * @returns {Promise<Object>} ${operation.method} ${operation.path} response`);
426
+ // Handle allOf, oneOf, anyOf
427
+ if (schema.allOf || schema.oneOf || schema.anyOf) {
428
+ return 'any'; // Could be expanded to intersection/union types
429
+ }
260
430
 
261
- return lines.join('\n');
431
+ return 'any';
262
432
  }
263
433
 
264
434
  /**
265
- * Get the number of operations in the OpenAPI spec
435
+ * Get TypeScript type for request body
266
436
  */
267
- getOperationsCount(): number {
268
- return this.extractOperations().length;
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';
269
455
  }
270
456
 
271
457
  /**
272
- * Generate method signature with options object
458
+ * Add request body properties directly to options array (flatten the body)
273
459
  */
274
- private generateMethodSignature(operation: OperationInfo): string {
275
- const pathParams: string[] = [];
276
- const queryParams: string[] = [];
277
- const hasBody = !!operation.requestBody;
460
+ private addRequestBodyProperties(requestBody: any, optionProps: string[]): void {
461
+ if (!requestBody) return;
278
462
 
279
- // Identify path and query parameters
280
- if (operation.parameters) {
281
- for (const param of operation.parameters) {
282
- if (typeof param === 'object' && 'name' in param && 'in' in param) {
283
- if (param.in === 'path') {
284
- pathParams.push(param.name);
285
- } else if (param.in === 'query') {
286
- queryParams.push(param.name);
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;
475
+ }
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
+ }
494
+
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}`);
287
512
  }
513
+ } else {
514
+ optionProps.push(`${propName}${optional}: ${propType}`);
288
515
  }
289
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}`);
290
521
  }
522
+ }
291
523
 
292
- // If there are no query params, no body, and only path params, use simple signature
293
- if (queryParams.length === 0 && !hasBody && pathParams.length <= 1) {
294
- const params: string[] = [];
295
- for (const paramName of pathParams) {
296
- params.push(`${paramName}: string`);
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
+ }
540
+
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
+ }
297
547
  }
298
- params.push(`options?: {headers?: {[key: string]: any}}`);
299
- return `(${params.join(', ')})`;
300
548
  }
301
549
 
302
- // Otherwise, use options object pattern
303
- return `(options?: {${[
304
- ...pathParams.map((p) => `${p}?: string`),
305
- ...queryParams.map((p) => `${p}?: any`),
306
- hasBody ? 'body?: any' : '',
307
- 'headers?: {[key: string]: any}',
308
- ]
309
- .filter(Boolean)
310
- .join(', ')}})`;
550
+ return 'any';
311
551
  }
312
552
 
313
553
  /**
314
- * Generate method implementation code
554
+ * Get TypeScript type for a parameter based on its schema
315
555
  */
316
- private generateMethodImplementation(operation: OperationInfo): string {
317
- const lines: string[] = [];
556
+ private getParameterType(param: any): string {
557
+ if (param.schema) {
558
+ return this.getTypeFromSchema(param.schema);
559
+ }
560
+ return 'any';
561
+ }
318
562
 
319
- // Build URL with path parameters
320
- 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;
321
569
  const pathParams: string[] = [];
322
570
  const queryParams: string[] = [];
323
571
  const hasBody = !!operation.requestBody;
324
572
 
325
- // 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
326
577
  if (operation.parameters) {
327
- for (const param of operation.parameters) {
328
- if (typeof param === 'object' && 'name' in param && 'in' in param) {
329
- if (param.in === 'path') {
330
- pathParams.push(param.name);
331
- } else if (param.in === 'query') {
332
- queryParams.push(param.name);
333
- }
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);
334
583
  }
335
- }
584
+ });
336
585
  }
337
586
 
338
- // Check if using simple signature (single path param, no query/body)
339
- 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;
340
589
 
341
- if (useSimpleSignature && pathParams.length === 1) {
342
- // Simple signature: (pathParam: string, options?: {headers?: ...})
343
- 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
344
593
  lines.push(` let url = '${url}';`);
345
- lines.push(` if (${paramName}) {`);
346
- lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
347
- 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
+ }
348
599
  lines.push('');
349
- 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 = {`);
350
612
  lines.push(` method: '${operation.method}',`);
351
- lines.push(` headers: options?.headers,`);
352
- lines.push(` });`);
353
- } else {
354
- // Options object pattern
355
- 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(` };`);
356
634
  lines.push('');
357
635
 
358
- // Replace path parameters
359
- if (pathParams.length > 0) {
360
- lines.push(` // Build URL with path parameters`);
361
- lines.push(` let url = '${url}';`);
362
- for (const paramName of pathParams) {
363
- lines.push(` if (options.${paramName}) {`);
364
- 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};`);
365
642
  lines.push(` }`);
366
643
  }
367
644
  lines.push('');
368
- } else {
369
- 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;`);
370
661
  lines.push('');
371
662
  }
372
663
 
@@ -379,13 +670,19 @@ export class OpenAPIToConnector {
379
670
  lines.push(` params: {},`);
380
671
  }
381
672
 
382
- // Add body if present
673
+ // Add body
383
674
  if (hasBody) {
384
- lines.push(` body: options.body,`);
675
+ if (actuallyHasOptions) {
676
+ lines.push(` body: requestBody,`);
677
+ } else {
678
+ lines.push(` body: undefined,`);
679
+ }
385
680
  }
386
681
 
387
- // Add headers if present
388
- lines.push(` headers: options.headers,`);
682
+ // Add headers only if we have options
683
+ if (actuallyHasOptions) {
684
+ lines.push(` headers: options.headers,`);
685
+ }
389
686
 
390
687
  lines.push(` };`);
391
688
  lines.push('');
@@ -400,24 +697,604 @@ export class OpenAPIToConnector {
400
697
  }
401
698
  lines.push('');
402
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 '';
403
877
 
404
- // Make the API call
405
- lines.push(` return this.api.fetch(url, fetchOptions);`);
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
+ }
406
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');
1017
+
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`);
1032
+ }
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');
407
1041
 
408
1042
  return lines.join('\n');
409
1043
  }
410
1044
 
411
1045
  /**
412
- * 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)
413
1155
  */
414
- private generateImportPath(relativePath: string): string {
415
- // For resource classes, we need to reference the compiled .mjs files
416
- return relativePath.endsWith('.mjs') ? relativePath : `${relativePath}.mjs`;
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
+ }
417
1198
  }
418
1199
 
419
1200
  /**
420
- * Generate a resource class (does NOT extend AbstractController, receives controller reference)
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
1285
+ */
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`;
1294
+ }
1295
+
1296
+ /**
1297
+ * Generate a resource file with exported functions (new pattern for proper introspection)
421
1298
  */
422
1299
  generateResourceClass(className: string): string {
423
1300
  const operations = this.extractOperations();
@@ -426,56 +1303,54 @@ export class OpenAPIToConnector {
426
1303
  throw new Error('No operations found in OpenAPI specification');
427
1304
  }
428
1305
 
429
- const methods = operations
1306
+ const resourceName = className.replace('Resource', '').toLowerCase();
1307
+
1308
+ const functions = operations
430
1309
  .map((operation) => {
431
1310
  const methodName = this.generateMethodName(operation);
432
- const jsdoc = this.generateJSDoc(operation);
1311
+ const jsdoc = this.generateDetailedJSDoc(operation);
433
1312
  const signature = this.generateMethodSignature(operation);
434
- const implementation = this.generateMethodImplementation(operation);
1313
+ const implementation = this.generateResourceFunctionImplementation(operation);
435
1314
 
436
- 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}`;
437
1316
  })
438
1317
  .join('\n\n');
439
1318
 
440
- return `import {AbstractController} from '@aloma.io/integration-sdk';
1319
+ // Generate actual TypeScript interfaces
1320
+ const interfaces = this.generateAllInterfaces();
441
1321
 
442
- export default class ${className} {
443
- private controller: AbstractController;
1322
+ return `// ${className} resource functions
1323
+ // These functions will be bound to the controller instance and accessible as ${resourceName}.method()
444
1324
 
445
- constructor(controller: AbstractController) {
446
- this.controller = controller;
447
- }
1325
+ ${interfaces}
448
1326
 
449
- private get api() {
450
- return this.controller['api'];
451
- }
452
-
453
- ${methods}
454
- }`;
1327
+ ${functions}`;
455
1328
  }
456
1329
 
457
1330
  /**
458
- * Generate a main controller that composes multiple resources
1331
+ * Generate a main controller that composes multiple resources using function binding
459
1332
  */
460
- 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 {
461
1337
  // Get base URL from servers if available
462
- 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';
463
1340
 
464
1341
  const imports = resources
465
- .map((resource) => `import ${resource.className} from '../resources/${resource.fileName}.mjs';`)
1342
+ .map((resource) => `import * as ${resource.fileName}Functions from '../resources/${resource.fileName}.mjs';`)
466
1343
  .join('\n');
467
1344
 
468
- const properties = resources
469
- .map((resource) => ` ${resource.className.toLowerCase().replace('resource', '')}!: ${resource.className};`)
470
- .join('\n');
1345
+ const properties = resources.map((resource) => ` ${resource.fileName}: any = {};`).join('\n');
471
1346
 
472
- const initializations = resources
473
- .map(
474
- (resource) =>
475
- ` this.${resource.className.toLowerCase().replace('resource', '')} = new ${resource.className}(this);`
476
- )
1347
+ const bindings = resources
1348
+ .map((resource) => ` this.bindResourceFunctions('${resource.fileName}', ${resource.fileName}Functions);`)
477
1349
  .join('\n');
478
1350
 
1351
+ // Generate exposed methods for each resource to enable API introspection
1352
+ const exposedMethods = this.generateExposedResourceMethods(resources, resourceSpecs);
1353
+
479
1354
  return `import {AbstractController} from '@aloma.io/integration-sdk';
480
1355
  ${imports}
481
1356
 
@@ -485,18 +1360,45 @@ ${properties}
485
1360
  private api: any;
486
1361
 
487
1362
  protected async start(): Promise<void> {
1363
+ const config = this.config;
1364
+
488
1365
  this.api = this.getClient({
489
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
+ },
490
1372
  });
491
1373
 
492
- // Initialize each resource - they receive 'this' controller reference
493
- ${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
+ }
494
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}
495
1397
  }`;
496
1398
  }
497
1399
 
498
1400
  /**
499
- * Generate the connector controller code
1401
+ * Generate the connector controller code with improved pattern
500
1402
  */
501
1403
  generateController(): string {
502
1404
  const operations = this.extractOperations();
@@ -508,28 +1410,42 @@ ${initializations}
508
1410
  const methods = operations
509
1411
  .map((operation) => {
510
1412
  const methodName = this.generateMethodName(operation);
511
- const jsdoc = this.generateJSDoc(operation);
1413
+ const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
512
1414
  const signature = this.generateMethodSignature(operation);
513
- const implementation = this.generateMethodImplementation(operation);
1415
+ const implementation = this.generateControllerMethodImplementation(operation); // Use improved implementation
514
1416
 
515
1417
  return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
516
1418
  })
517
1419
  .join('\n\n');
518
1420
 
519
1421
  // Get base URL from servers if available
520
- 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();
521
1430
 
522
1431
  const startMethod = ` private api: any;
523
1432
 
524
1433
  protected async start(): Promise<void> {
1434
+ const config = this.config;
1435
+
525
1436
  this.api = this.getClient({
526
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
+ },
527
1443
  });
528
1444
  }`;
529
1445
 
530
1446
  return `import {AbstractController} from '@aloma.io/integration-sdk';
531
1447
 
532
- export default class Controller extends AbstractController {
1448
+ ${typeImports}${interfaces}export default class Controller extends AbstractController {
533
1449
 
534
1450
  ${startMethod}
535
1451