@aloma.io/integration-sdk 3.8.52 → 3.8.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aloma.io/integration-sdk",
3
- "version": "3.8.52",
3
+ "version": "3.8.54",
4
4
  "description": "",
5
5
  "author": "aloma.io",
6
6
  "license": "Apache-2.0",
package/src/cli.mts CHANGED
@@ -139,6 +139,12 @@ program
139
139
  .requiredOption('--connector-id <id>', 'id of the connector')
140
140
  .requiredOption('--spec <file>', 'OpenAPI specification file (JSON or YAML)')
141
141
  .option('--out <file>', 'output file path for the controller', 'src/controller/index.mts')
142
+ .option(
143
+ '--resource <className>',
144
+ 'Generate as a resource class with the specified class name (e.g., CompaniesResource)'
145
+ )
146
+ .option('--multi-resource', 'Generate multiple resource files + main controller (requires multiple --spec files)')
147
+ .option('--no-build', 'Skip installing dependencies and building the project')
142
148
  .action(async (name, options) => {
143
149
  name = name.replace(/[\/\.]/gi, '');
144
150
  if (!name) throw new Error('name is empty');
@@ -156,9 +162,16 @@ program
156
162
  extract({...options, target, name});
157
163
 
158
164
  // Generate the controller from OpenAPI spec
159
- console.log('Generating controller from OpenAPI specification...');
160
165
  const generator = new OpenAPIToConnector(spec, name);
161
- const controllerCode = generator.generateController();
166
+ let controllerCode: string;
167
+
168
+ if (options.resource) {
169
+ console.log(`Generating resource class '${options.resource}' from OpenAPI specification...`);
170
+ controllerCode = generator.generateResourceClass(options.resource);
171
+ } else {
172
+ console.log('Generating controller from OpenAPI specification...');
173
+ controllerCode = generator.generateController();
174
+ }
162
175
 
163
176
  // Write the generated controller
164
177
  const controllerPath = `${target}/${options.out}`;
@@ -168,11 +181,28 @@ program
168
181
  console.log('Generating keys...');
169
182
  await generateKeys({target});
170
183
 
171
- console.log('Installing dependencies...');
172
- await exec(`cd ${target}; yarn --ignore-engines`);
184
+ if (options.build !== false) {
185
+ console.log('Installing dependencies...');
186
+ await exec(`cd ${target}; yarn --ignore-engines`);
173
187
 
174
- console.log('Building...');
175
- await exec(`cd ${target}; yarn build`);
188
+ console.log('Building...');
189
+ await exec(`cd ${target}; yarn build`);
190
+ }
191
+
192
+ const nextSteps =
193
+ options.build !== false
194
+ ? `Next steps:
195
+ 1.) Add the connector to a workspace
196
+ 2.) Edit ./${name}/.env and insert the registration token
197
+ 3.) Implement the actual API calls in each method in ${options.out}
198
+ 4.) Start the connector with cd ./${name}/; yarn start`
199
+ : `Next steps:
200
+ 1.) Install dependencies: cd ./${name}/ && yarn --ignore-engines
201
+ 2.) Implement the actual API calls in each method in ${options.out}
202
+ 3.) Build the project: yarn build
203
+ 4.) Add the connector to a workspace
204
+ 5.) Edit ./${name}/.env and insert the registration token
205
+ 6.) Start the connector: yarn start`;
176
206
 
177
207
  console.log(`
178
208
  ✅ Success! Generated connector from OpenAPI specification
@@ -180,11 +210,7 @@ program
180
210
  📊 Found ${generator.getOperationsCount()} operations
181
211
  📄 Controller generated: ${options.out}
182
212
 
183
- Next steps:
184
- 1.) Add the connector to a workspace
185
- 2.) Edit ./${name}/.env and insert the registration token
186
- 3.) Implement the actual API calls in each method in ${options.out}
187
- 4.) Start the connector with cd ./${name}/; yarn start`);
213
+ ${nextSteps}`);
188
214
  } catch (error) {
189
215
  console.error('❌ Error:', error instanceof Error ? error.message : 'Unknown error');
190
216
  // Clean up on error
@@ -221,4 +247,162 @@ class Extractor {
221
247
  }
222
248
  }
223
249
 
250
+ // Multi-resource connector creation
251
+ program
252
+ .command('create-multi-resource')
253
+ .description('Create a multi-resource connector project with multiple OpenAPI specifications')
254
+ .argument('<name>', 'name of the connector project')
255
+ .requiredOption('--connector-id <id>', 'id of the connector')
256
+ .requiredOption(
257
+ '--resources <specs>',
258
+ 'comma-separated list of "className:specFile" pairs (e.g., "CompaniesResource:companies.json,ContactsResource:contacts.json")'
259
+ )
260
+ .option('--base-url <url>', 'base URL for the API (if not specified, will be extracted from first OpenAPI spec)')
261
+ .option('--no-build', 'Skip installing dependencies and building the project')
262
+ .action(async (name, options) => {
263
+ name = name.replace(/[\/\.]/gi, '');
264
+ if (!name) throw new Error('name is empty');
265
+
266
+ const target = `${process.cwd()}/${name}`;
267
+
268
+ try {
269
+ // Parse resources specification
270
+ const resourceSpecs = options.resources.split(',').map((spec) => {
271
+ const [className, specFile] = spec.split(':');
272
+ if (!className || !specFile) {
273
+ throw new Error(`Invalid resource specification: ${spec}. Expected format: "ClassName:specFile"`);
274
+ }
275
+ return {className: className.trim(), specFile: specFile.trim()};
276
+ });
277
+
278
+ console.log(`Creating multi-resource connector '${name}' with ${resourceSpecs.length} resources...`);
279
+
280
+ // Create the connector project structure
281
+ fs.mkdirSync(target);
282
+ extract({...options, target, name});
283
+
284
+ // Generate each resource
285
+ const resources: Array<{className: string; fileName: string}> = [];
286
+ let baseUrl = options.baseUrl;
287
+
288
+ for (const {className, specFile} of resourceSpecs) {
289
+ console.log(`Generating ${className} from ${specFile}...`);
290
+
291
+ // Read and parse the OpenAPI spec
292
+ const specContent = fs.readFileSync(specFile, 'utf-8');
293
+ const spec = OpenAPIToConnector.parseSpec(specContent);
294
+
295
+ // Extract base URL from first spec if not provided
296
+ if (!baseUrl && spec.servers && spec.servers.length > 0) {
297
+ baseUrl = spec.servers[0].url;
298
+ }
299
+
300
+ // Generate the resource class
301
+ const generator = new OpenAPIToConnector(spec, name);
302
+ const resourceCode = generator.generateResourceClass(className);
303
+
304
+ // Write the resource file
305
+ const fileName = className.toLowerCase().replace('resource', '');
306
+ const resourcePath = `${target}/src/resources/${fileName}.mts`;
307
+ fs.mkdirSync(path.dirname(resourcePath), {recursive: true});
308
+ fs.writeFileSync(resourcePath, resourceCode);
309
+
310
+ resources.push({className, fileName});
311
+ }
312
+
313
+ // Generate the main controller
314
+ console.log('Generating main controller...');
315
+ const firstSpec = OpenAPIToConnector.parseSpec(fs.readFileSync(resourceSpecs[0].specFile, 'utf-8'));
316
+ const mainGenerator = new OpenAPIToConnector(firstSpec, name);
317
+ const mainControllerCode = mainGenerator.generateMainController(resources);
318
+
319
+ // Write the main controller
320
+ const controllerPath = `${target}/src/controller/index.mts`;
321
+ fs.writeFileSync(controllerPath, mainControllerCode);
322
+
323
+ console.log('Generating keys...');
324
+ await generateKeys({target});
325
+
326
+ if (options.build !== false) {
327
+ console.log('Installing dependencies...');
328
+ await exec(`cd "${target}"; yarn --ignore-engines`);
329
+
330
+ console.log('Building...');
331
+ await exec(`cd "${target}"; yarn build`);
332
+ }
333
+
334
+ const nextSteps =
335
+ options.build !== false
336
+ ? `Next steps:
337
+ 1.) Add the connector to a workspace
338
+ 2.) Edit ./${name}/.env and insert the registration token
339
+ 3.) Start the connector with cd ./${name}/; yarn start`
340
+ : `Next steps:
341
+ 1.) Install dependencies: cd ./${name}/ && yarn --ignore-engines
342
+ 2.) Build the project: yarn build
343
+ 3.) Add the connector to a workspace
344
+ 4.) Edit ./${name}/.env and insert the registration token
345
+ 5.) Start the connector with yarn start`;
346
+
347
+ console.log(`\n✅ Multi-resource connector created successfully!
348
+
349
+ Generated resources:
350
+ ${resources.map((r) => `- ${r.className} (${r.fileName}.mts)`).join('\n')}
351
+
352
+ Main controller: src/controller/index.mts
353
+ ${nextSteps}`);
354
+ } catch (error) {
355
+ console.error('Error creating multi-resource connector:', (error as Error).message);
356
+ process.exit(1);
357
+ }
358
+ });
359
+
360
+ // Add resource to existing project
361
+ program
362
+ .command('add-resource')
363
+ .description('Add a new resource to an existing multi-resource connector')
364
+ .argument('<projectPath>', 'path to the existing connector project')
365
+ .requiredOption('--className <name>', 'class name for the resource (e.g., DealsResource)')
366
+ .requiredOption('--spec <file>', 'OpenAPI specification file for the new resource')
367
+ .option('--no-build', 'Skip building the project after adding the resource')
368
+ .action(async (projectPath, options) => {
369
+ const target = path.resolve(projectPath);
370
+
371
+ if (!fs.existsSync(target)) {
372
+ throw new Error(`Project path does not exist: ${target}`);
373
+ }
374
+
375
+ try {
376
+ console.log(`Adding ${options.className} resource to existing project...`);
377
+
378
+ // Read and parse the OpenAPI spec
379
+ const specContent = fs.readFileSync(options.spec, 'utf-8');
380
+ const spec = OpenAPIToConnector.parseSpec(specContent);
381
+
382
+ // Generate the resource class
383
+ const generator = new OpenAPIToConnector(spec, 'Resource');
384
+ const resourceCode = generator.generateResourceClass(options.className);
385
+
386
+ // Write the resource file
387
+ const fileName = options.className.toLowerCase().replace('resource', '');
388
+ const resourcePath = `${target}/src/resources/${fileName}.mts`;
389
+ fs.mkdirSync(path.dirname(resourcePath), {recursive: true});
390
+ fs.writeFileSync(resourcePath, resourceCode);
391
+
392
+ console.log(`✅ Resource ${options.className} added successfully at ${resourcePath}`);
393
+ console.log('\n⚠️ You need to manually update the main controller to include this new resource:');
394
+ console.log(`1.) Add import: import ${options.className} from '../resources/${fileName}.mjs';`);
395
+ console.log(`2.) Add property: ${fileName}!: ${options.className};`);
396
+ console.log(`3.) Add initialization in start(): this.${fileName} = new ${options.className}(this);`);
397
+
398
+ if (options.build !== false) {
399
+ console.log('\nBuilding project...');
400
+ await exec(`cd "${target}"; yarn build`);
401
+ }
402
+ } catch (error) {
403
+ console.error('Error adding resource:', (error as Error).message);
404
+ process.exit(1);
405
+ }
406
+ });
407
+
224
408
  program.parse();
@@ -65,11 +65,23 @@ export class OpenAPIToConnector {
65
65
  }
66
66
  }
67
67
 
68
- // Validate against OpenAPI 3.x schema
68
+ // Validate against OpenAPI 3.x schema with lenient validation
69
69
  const validationResult = OpenAPISchema.safeParse(parsed);
70
70
  if (!validationResult.success) {
71
- const errors = validationResult.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ');
72
- throw new Error(`Invalid OpenAPI 3.x specification: ${errors}`);
71
+ // Check if the errors are just about missing 'type' fields in schemas
72
+ const criticalErrors = validationResult.error.errors.filter((err) => {
73
+ const path = err.path.join('.');
74
+ // Allow missing 'type' in schema definitions as many OpenAPI specs don't include it
75
+ return !path.includes('components.schemas') || !err.message.includes('Required');
76
+ });
77
+
78
+ if (criticalErrors.length > 0) {
79
+ const errors = criticalErrors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ');
80
+ throw new Error(`Invalid OpenAPI 3.x specification: ${errors}`);
81
+ }
82
+
83
+ // Log a warning about lenient validation
84
+ console.warn('⚠️ OpenAPI spec has some validation warnings but proceeding with lenient validation...');
73
85
  }
74
86
 
75
87
  return parsed as OpenAPIV3.Document;
@@ -124,7 +136,7 @@ export class OpenAPIToConnector {
124
136
  if (operation.operationId) {
125
137
  // Clean up HubSpot-style operationIds like "get-/crm/v3/objects/companies_getPage"
126
138
  let cleaned = operation.operationId;
127
-
139
+
128
140
  // Extract the last part after underscore if it exists
129
141
  const parts = cleaned.split('_');
130
142
  if (parts.length > 1) {
@@ -134,14 +146,14 @@ export class OpenAPIToConnector {
134
146
  cleaned = lastPart;
135
147
  }
136
148
  }
137
-
149
+
138
150
  // Remove any remaining special characters and clean up
139
151
  cleaned = cleaned
140
152
  .replace(/^(get|post|put|patch|delete|head|options)-/i, '') // Remove HTTP method prefix
141
153
  .replace(/[^a-zA-Z0-9_]/g, '_')
142
154
  .replace(/_+/g, '_')
143
155
  .replace(/^_|_$/g, '');
144
-
156
+
145
157
  // If we still have a valid identifier, use it
146
158
  if (cleaned && /^[a-zA-Z]/.test(cleaned)) {
147
159
  return cleaned;
@@ -166,6 +178,9 @@ export class OpenAPIToConnector {
166
178
  */
167
179
  private generateJSDoc(operation: OperationInfo): string {
168
180
  const lines: string[] = [];
181
+ const pathParams: any[] = [];
182
+ const queryParams: any[] = [];
183
+ const hasBody = !!operation.requestBody;
169
184
 
170
185
  if (operation.summary) {
171
186
  lines.push(` * ${operation.summary}`);
@@ -175,44 +190,68 @@ export class OpenAPIToConnector {
175
190
  lines.push(` *`);
176
191
  // Split long descriptions into multiple lines
177
192
  const descLines = operation.description.split('\n');
178
- descLines.forEach(line => {
193
+ descLines.forEach((line) => {
179
194
  if (line.trim()) {
180
195
  lines.push(` * ${line.trim()}`);
181
196
  }
182
197
  });
183
198
  }
184
199
 
185
- // Document parameters with full details
186
- if (operation.parameters && operation.parameters.length > 0) {
187
- lines.push(' *');
188
- lines.push(' * @param {Object} args - Request arguments');
189
-
200
+ // Identify path and query parameters
201
+ if (operation.parameters) {
190
202
  for (const param of operation.parameters) {
191
- if (typeof param === 'object' && 'name' in param) {
192
- const paramName = param.name;
193
- const paramDesc = param.description || '';
194
- const paramRequired = param.required ? '(required)' : '(optional)';
195
- const paramType = param.schema?.type || 'any';
196
- const paramIn = param.in || '';
197
-
198
- let paramDoc = ` * @param {${paramType}} args.${paramName} ${paramRequired}`;
199
- if (paramDesc) {
200
- paramDoc += ` - ${paramDesc}`;
201
- }
202
- if (paramIn) {
203
- paramDoc += ` [${paramIn}]`;
203
+ if (typeof param === 'object' && 'name' in param && 'in' in param) {
204
+ if (param.in === 'path') {
205
+ pathParams.push(param);
206
+ } else if (param.in === 'query') {
207
+ queryParams.push(param);
204
208
  }
205
- lines.push(paramDoc);
206
209
  }
207
210
  }
208
211
  }
209
212
 
210
- // Document request body
211
- if (operation.requestBody) {
213
+ // Check if using simple signature
214
+ const useSimpleSignature = queryParams.length === 0 && !hasBody && pathParams.length <= 1;
215
+
216
+ if (useSimpleSignature && pathParams.length === 1) {
217
+ // Simple signature documentation
218
+ const param = pathParams[0];
219
+ const paramType = param.schema?.type || 'string';
220
+ const paramDesc = param.description || '';
221
+ lines.push(' *');
222
+ lines.push(` * @param {${paramType}} ${param.name} ${paramDesc}`);
223
+ lines.push(` * @param {Object} options (optional) - Request options`);
224
+ lines.push(` * @param {Object} options.headers - Custom headers`);
225
+ } else {
226
+ // Options object documentation
212
227
  lines.push(' *');
213
- const bodyDesc = operation.requestBody.description || 'Request body';
214
- const required = operation.requestBody.required ? '(required)' : '(optional)';
215
- lines.push(` * @param {Object} args.body ${required} - ${bodyDesc}`);
228
+ lines.push(` * @param {Object} options (optional) - Request options`);
229
+
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]`);
236
+ }
237
+
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]`);
244
+ }
245
+
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}`);
251
+ }
252
+
253
+ // Document headers
254
+ lines.push(` * @param {Object} options.headers (optional) - Custom headers to include in the request`);
216
255
  }
217
256
 
218
257
  // Document response
@@ -229,6 +268,233 @@ export class OpenAPIToConnector {
229
268
  return this.extractOperations().length;
230
269
  }
231
270
 
271
+ /**
272
+ * Generate method signature with options object
273
+ */
274
+ private generateMethodSignature(operation: OperationInfo): string {
275
+ const pathParams: string[] = [];
276
+ const queryParams: string[] = [];
277
+ const hasBody = !!operation.requestBody;
278
+
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);
287
+ }
288
+ }
289
+ }
290
+ }
291
+
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`);
297
+ }
298
+ params.push(`options?: {headers?: {[key: string]: any}}`);
299
+ return `(${params.join(', ')})`;
300
+ }
301
+
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(', ')}})`;
311
+ }
312
+
313
+ /**
314
+ * Generate method implementation code
315
+ */
316
+ private generateMethodImplementation(operation: OperationInfo): string {
317
+ const lines: string[] = [];
318
+
319
+ // Build URL with path parameters
320
+ let url = operation.path;
321
+ const pathParams: string[] = [];
322
+ const queryParams: string[] = [];
323
+ const hasBody = !!operation.requestBody;
324
+
325
+ // Identify path and query parameters
326
+ 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
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // Check if using simple signature (single path param, no query/body)
339
+ const useSimpleSignature = queryParams.length === 0 && !hasBody && pathParams.length <= 1;
340
+
341
+ if (useSimpleSignature && pathParams.length === 1) {
342
+ // Simple signature: (pathParam: string, options?: {headers?: ...})
343
+ const paramName = pathParams[0];
344
+ lines.push(` let url = '${url}';`);
345
+ lines.push(` if (${paramName}) {`);
346
+ lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
347
+ lines.push(` }`);
348
+ lines.push('');
349
+ lines.push(` return this.api.fetch(url, {`);
350
+ 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 || {};`);
356
+ lines.push('');
357
+
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});`);
365
+ lines.push(` }`);
366
+ }
367
+ lines.push('');
368
+ } else {
369
+ lines.push(` const url = '${url}';`);
370
+ lines.push('');
371
+ }
372
+
373
+ // Build fetch options
374
+ lines.push(` const fetchOptions: any = {`);
375
+ lines.push(` method: '${operation.method}',`);
376
+
377
+ // Add query parameters
378
+ if (queryParams.length > 0) {
379
+ lines.push(` params: {},`);
380
+ }
381
+
382
+ // Add body if present
383
+ if (hasBody) {
384
+ lines.push(` body: options.body,`);
385
+ }
386
+
387
+ // Add headers if present
388
+ lines.push(` headers: options.headers,`);
389
+
390
+ lines.push(` };`);
391
+ lines.push('');
392
+
393
+ // Add query parameters to options
394
+ if (queryParams.length > 0) {
395
+ lines.push(` // Add query parameters`);
396
+ for (const paramName of queryParams) {
397
+ lines.push(` if (options.${paramName} !== undefined) {`);
398
+ lines.push(` fetchOptions.params.${paramName} = options.${paramName};`);
399
+ lines.push(` }`);
400
+ }
401
+ lines.push('');
402
+ }
403
+
404
+ // Make the API call
405
+ lines.push(` return this.api.fetch(url, fetchOptions);`);
406
+ }
407
+
408
+ return lines.join('\n');
409
+ }
410
+
411
+ /**
412
+ * Generate proper import paths with .mjs extensions for TypeScript module resolution
413
+ */
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`;
417
+ }
418
+
419
+ /**
420
+ * Generate a resource class (does NOT extend AbstractController, receives controller reference)
421
+ */
422
+ generateResourceClass(className: string): string {
423
+ const operations = this.extractOperations();
424
+
425
+ if (operations.length === 0) {
426
+ throw new Error('No operations found in OpenAPI specification');
427
+ }
428
+
429
+ const methods = operations
430
+ .map((operation) => {
431
+ const methodName = this.generateMethodName(operation);
432
+ const jsdoc = this.generateJSDoc(operation);
433
+ const signature = this.generateMethodSignature(operation);
434
+ const implementation = this.generateMethodImplementation(operation);
435
+
436
+ return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
437
+ })
438
+ .join('\n\n');
439
+
440
+ return `import {AbstractController} from '@aloma.io/integration-sdk';
441
+
442
+ export default class ${className} {
443
+ private controller: AbstractController;
444
+
445
+ constructor(controller: AbstractController) {
446
+ this.controller = controller;
447
+ }
448
+
449
+ private get api() {
450
+ return this.controller['api'];
451
+ }
452
+
453
+ ${methods}
454
+ }`;
455
+ }
456
+
457
+ /**
458
+ * Generate a main controller that composes multiple resources
459
+ */
460
+ generateMainController(resources: Array<{className: string; fileName: string}>): string {
461
+ // 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';
463
+
464
+ const imports = resources
465
+ .map((resource) => `import ${resource.className} from '../resources/${resource.fileName}.mjs';`)
466
+ .join('\n');
467
+
468
+ const properties = resources
469
+ .map((resource) => ` ${resource.className.toLowerCase().replace('resource', '')}!: ${resource.className};`)
470
+ .join('\n');
471
+
472
+ const initializations = resources
473
+ .map(
474
+ (resource) =>
475
+ ` this.${resource.className.toLowerCase().replace('resource', '')} = new ${resource.className}(this);`
476
+ )
477
+ .join('\n');
478
+
479
+ return `import {AbstractController} from '@aloma.io/integration-sdk';
480
+ ${imports}
481
+
482
+ export default class Controller extends AbstractController {
483
+ ${properties}
484
+
485
+ private api: any;
486
+
487
+ protected async start(): Promise<void> {
488
+ this.api = this.getClient({
489
+ baseUrl: '${baseUrl}',
490
+ });
491
+
492
+ // Initialize each resource - they receive 'this' controller reference
493
+ ${initializations}
494
+ }
495
+ }`;
496
+ }
497
+
232
498
  /**
233
499
  * Generate the connector controller code
234
500
  */
@@ -243,15 +509,30 @@ export class OpenAPIToConnector {
243
509
  .map((operation) => {
244
510
  const methodName = this.generateMethodName(operation);
245
511
  const jsdoc = this.generateJSDoc(operation);
512
+ const signature = this.generateMethodSignature(operation);
513
+ const implementation = this.generateMethodImplementation(operation);
246
514
 
247
- return ` /**\n${jsdoc}\n */\n async ${methodName}(args: any) {\n // TODO: Implement ${operation.method} ${operation.path}\n throw new Error('Method not implemented');\n }`;
515
+ return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
248
516
  })
249
517
  .join('\n\n');
250
518
 
519
+ // 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';
521
+
522
+ const startMethod = ` private api: any;
523
+
524
+ protected async start(): Promise<void> {
525
+ this.api = this.getClient({
526
+ baseUrl: '${baseUrl}',
527
+ });
528
+ }`;
529
+
251
530
  return `import {AbstractController} from '@aloma.io/integration-sdk';
252
531
 
253
532
  export default class Controller extends AbstractController {
254
533
 
534
+ ${startMethod}
535
+
255
536
  ${methods}
256
537
  }`;
257
538
  }