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