@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/MULTI_RESOURCE_GUIDE.md +217 -0
- package/README.md +55 -2
- package/build/cli.mjs +162 -11
- package/build/openapi-to-connector.d.mts +23 -0
- package/build/openapi-to-connector.mjs +274 -26
- package/examples/api-without-servers.json +32 -0
- package/examples/companies-resource-class.mts +310 -0
- package/examples/companies-resource.mts +310 -0
- package/examples/complete-example.sh +116 -0
- package/examples/create-hubspot-connector.sh +33 -0
- package/examples/hubspot-companies.json +1889 -0
- package/examples/hubspot-contacts.json +1919 -0
- package/examples/hubspot-controller-individual-params.mts +323 -0
- package/examples/hubspot-controller-with-implementation.mts +315 -0
- package/examples/hubspot-controller.mts +192 -0
- package/examples/hubspot-lists.json +5525 -0
- package/examples/main-controller-with-resources.mts +35 -0
- package/examples/stripe.json +182829 -0
- package/examples/utility-click.json +8992 -0
- package/package.json +1 -1
- package/src/cli.mts +195 -11
- package/src/openapi-to-connector.mts +313 -32
package/package.json
CHANGED
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
|
-
|
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
|
-
|
172
|
-
|
184
|
+
if (options.build !== false) {
|
185
|
+
console.log('Installing dependencies...');
|
186
|
+
await exec(`cd ${target}; yarn --ignore-engines`);
|
173
187
|
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
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
|
-
//
|
186
|
-
if (operation.parameters
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
//
|
211
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
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}
|
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
|
}
|