@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
@@ -45,11 +45,21 @@ export class OpenAPIToConnector {
|
|
45
45
|
throw new Error(`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
46
46
|
}
|
47
47
|
}
|
48
|
-
// Validate against OpenAPI 3.x schema
|
48
|
+
// Validate against OpenAPI 3.x schema with lenient validation
|
49
49
|
const validationResult = OpenAPISchema.safeParse(parsed);
|
50
50
|
if (!validationResult.success) {
|
51
|
-
|
52
|
-
|
51
|
+
// Check if the errors are just about missing 'type' fields in schemas
|
52
|
+
const criticalErrors = validationResult.error.errors.filter((err) => {
|
53
|
+
const path = err.path.join('.');
|
54
|
+
// Allow missing 'type' in schema definitions as many OpenAPI specs don't include it
|
55
|
+
return !path.includes('components.schemas') || !err.message.includes('Required');
|
56
|
+
});
|
57
|
+
if (criticalErrors.length > 0) {
|
58
|
+
const errors = criticalErrors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ');
|
59
|
+
throw new Error(`Invalid OpenAPI 3.x specification: ${errors}`);
|
60
|
+
}
|
61
|
+
// Log a warning about lenient validation
|
62
|
+
console.warn('⚠️ OpenAPI spec has some validation warnings but proceeding with lenient validation...');
|
53
63
|
}
|
54
64
|
return parsed;
|
55
65
|
}
|
@@ -132,6 +142,9 @@ export class OpenAPIToConnector {
|
|
132
142
|
*/
|
133
143
|
generateJSDoc(operation) {
|
134
144
|
const lines = [];
|
145
|
+
const pathParams = [];
|
146
|
+
const queryParams = [];
|
147
|
+
const hasBody = !!operation.requestBody;
|
135
148
|
if (operation.summary) {
|
136
149
|
lines.push(` * ${operation.summary}`);
|
137
150
|
}
|
@@ -139,40 +152,63 @@ export class OpenAPIToConnector {
|
|
139
152
|
lines.push(` *`);
|
140
153
|
// Split long descriptions into multiple lines
|
141
154
|
const descLines = operation.description.split('\n');
|
142
|
-
descLines.forEach(line => {
|
155
|
+
descLines.forEach((line) => {
|
143
156
|
if (line.trim()) {
|
144
157
|
lines.push(` * ${line.trim()}`);
|
145
158
|
}
|
146
159
|
});
|
147
160
|
}
|
148
|
-
//
|
149
|
-
if (operation.parameters
|
150
|
-
lines.push(' *');
|
151
|
-
lines.push(' * @param {Object} args - Request arguments');
|
161
|
+
// Identify path and query parameters
|
162
|
+
if (operation.parameters) {
|
152
163
|
for (const param of operation.parameters) {
|
153
|
-
if (typeof param === 'object' && 'name' in param) {
|
154
|
-
|
155
|
-
|
156
|
-
const paramRequired = param.required ? '(required)' : '(optional)';
|
157
|
-
const paramType = param.schema?.type || 'any';
|
158
|
-
const paramIn = param.in || '';
|
159
|
-
let paramDoc = ` * @param {${paramType}} args.${paramName} ${paramRequired}`;
|
160
|
-
if (paramDesc) {
|
161
|
-
paramDoc += ` - ${paramDesc}`;
|
164
|
+
if (typeof param === 'object' && 'name' in param && 'in' in param) {
|
165
|
+
if (param.in === 'path') {
|
166
|
+
pathParams.push(param);
|
162
167
|
}
|
163
|
-
if (
|
164
|
-
|
168
|
+
else if (param.in === 'query') {
|
169
|
+
queryParams.push(param);
|
165
170
|
}
|
166
|
-
lines.push(paramDoc);
|
167
171
|
}
|
168
172
|
}
|
169
173
|
}
|
170
|
-
//
|
171
|
-
|
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`);
|
185
|
+
}
|
186
|
+
else {
|
187
|
+
// Options object documentation
|
172
188
|
lines.push(' *');
|
173
|
-
|
174
|
-
|
175
|
-
|
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]`);
|
196
|
+
}
|
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]`);
|
203
|
+
}
|
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}`);
|
209
|
+
}
|
210
|
+
// Document headers
|
211
|
+
lines.push(` * @param {Object} options.headers (optional) - Custom headers to include in the request`);
|
176
212
|
}
|
177
213
|
// Document response
|
178
214
|
lines.push(' *');
|
@@ -185,6 +221,205 @@ export class OpenAPIToConnector {
|
|
185
221
|
getOperationsCount() {
|
186
222
|
return this.extractOperations().length;
|
187
223
|
}
|
224
|
+
/**
|
225
|
+
* Generate method signature with options object
|
226
|
+
*/
|
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);
|
237
|
+
}
|
238
|
+
else if (param.in === 'query') {
|
239
|
+
queryParams.push(param.name);
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}
|
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`);
|
249
|
+
}
|
250
|
+
params.push(`options?: {headers?: {[key: string]: any}}`);
|
251
|
+
return `(${params.join(', ')})`;
|
252
|
+
}
|
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(', ')}})`;
|
262
|
+
}
|
263
|
+
/**
|
264
|
+
* Generate method implementation code
|
265
|
+
*/
|
266
|
+
generateMethodImplementation(operation) {
|
267
|
+
const lines = [];
|
268
|
+
// Build URL with path parameters
|
269
|
+
let url = operation.path;
|
270
|
+
const pathParams = [];
|
271
|
+
const queryParams = [];
|
272
|
+
const hasBody = !!operation.requestBody;
|
273
|
+
// Identify path and query parameters
|
274
|
+
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
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
}
|
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];
|
291
|
+
lines.push(` let url = '${url}';`);
|
292
|
+
lines.push(` if (${paramName}) {`);
|
293
|
+
lines.push(` url = url.replace('{${paramName}}', ${paramName});`);
|
294
|
+
lines.push(` }`);
|
295
|
+
lines.push('');
|
296
|
+
lines.push(` return this.api.fetch(url, {`);
|
297
|
+
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 || {};`);
|
304
|
+
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});`);
|
312
|
+
lines.push(` }`);
|
313
|
+
}
|
314
|
+
lines.push('');
|
315
|
+
}
|
316
|
+
else {
|
317
|
+
lines.push(` const url = '${url}';`);
|
318
|
+
lines.push('');
|
319
|
+
}
|
320
|
+
// Build fetch options
|
321
|
+
lines.push(` const fetchOptions: any = {`);
|
322
|
+
lines.push(` method: '${operation.method}',`);
|
323
|
+
// Add query parameters
|
324
|
+
if (queryParams.length > 0) {
|
325
|
+
lines.push(` params: {},`);
|
326
|
+
}
|
327
|
+
// Add body if present
|
328
|
+
if (hasBody) {
|
329
|
+
lines.push(` body: options.body,`);
|
330
|
+
}
|
331
|
+
// Add headers if present
|
332
|
+
lines.push(` headers: options.headers,`);
|
333
|
+
lines.push(` };`);
|
334
|
+
lines.push('');
|
335
|
+
// Add query parameters to options
|
336
|
+
if (queryParams.length > 0) {
|
337
|
+
lines.push(` // Add query parameters`);
|
338
|
+
for (const paramName of queryParams) {
|
339
|
+
lines.push(` if (options.${paramName} !== undefined) {`);
|
340
|
+
lines.push(` fetchOptions.params.${paramName} = options.${paramName};`);
|
341
|
+
lines.push(` }`);
|
342
|
+
}
|
343
|
+
lines.push('');
|
344
|
+
}
|
345
|
+
// Make the API call
|
346
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
347
|
+
}
|
348
|
+
return lines.join('\n');
|
349
|
+
}
|
350
|
+
/**
|
351
|
+
* Generate proper import paths with .mjs extensions for TypeScript module resolution
|
352
|
+
*/
|
353
|
+
generateImportPath(relativePath) {
|
354
|
+
// For resource classes, we need to reference the compiled .mjs files
|
355
|
+
return relativePath.endsWith('.mjs') ? relativePath : `${relativePath}.mjs`;
|
356
|
+
}
|
357
|
+
/**
|
358
|
+
* Generate a resource class (does NOT extend AbstractController, receives controller reference)
|
359
|
+
*/
|
360
|
+
generateResourceClass(className) {
|
361
|
+
const operations = this.extractOperations();
|
362
|
+
if (operations.length === 0) {
|
363
|
+
throw new Error('No operations found in OpenAPI specification');
|
364
|
+
}
|
365
|
+
const methods = operations
|
366
|
+
.map((operation) => {
|
367
|
+
const methodName = this.generateMethodName(operation);
|
368
|
+
const jsdoc = this.generateJSDoc(operation);
|
369
|
+
const signature = this.generateMethodSignature(operation);
|
370
|
+
const implementation = this.generateMethodImplementation(operation);
|
371
|
+
return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
|
372
|
+
})
|
373
|
+
.join('\n\n');
|
374
|
+
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
375
|
+
|
376
|
+
export default class ${className} {
|
377
|
+
private controller: AbstractController;
|
378
|
+
|
379
|
+
constructor(controller: AbstractController) {
|
380
|
+
this.controller = controller;
|
381
|
+
}
|
382
|
+
|
383
|
+
private get api() {
|
384
|
+
return this.controller['api'];
|
385
|
+
}
|
386
|
+
|
387
|
+
${methods}
|
388
|
+
}`;
|
389
|
+
}
|
390
|
+
/**
|
391
|
+
* Generate a main controller that composes multiple resources
|
392
|
+
*/
|
393
|
+
generateMainController(resources) {
|
394
|
+
// 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';
|
396
|
+
const imports = resources
|
397
|
+
.map((resource) => `import ${resource.className} from '../resources/${resource.fileName}.mjs';`)
|
398
|
+
.join('\n');
|
399
|
+
const properties = resources
|
400
|
+
.map((resource) => ` ${resource.className.toLowerCase().replace('resource', '')}!: ${resource.className};`)
|
401
|
+
.join('\n');
|
402
|
+
const initializations = resources
|
403
|
+
.map((resource) => ` this.${resource.className.toLowerCase().replace('resource', '')} = new ${resource.className}(this);`)
|
404
|
+
.join('\n');
|
405
|
+
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
406
|
+
${imports}
|
407
|
+
|
408
|
+
export default class Controller extends AbstractController {
|
409
|
+
${properties}
|
410
|
+
|
411
|
+
private api: any;
|
412
|
+
|
413
|
+
protected async start(): Promise<void> {
|
414
|
+
this.api = this.getClient({
|
415
|
+
baseUrl: '${baseUrl}',
|
416
|
+
});
|
417
|
+
|
418
|
+
// Initialize each resource - they receive 'this' controller reference
|
419
|
+
${initializations}
|
420
|
+
}
|
421
|
+
}`;
|
422
|
+
}
|
188
423
|
/**
|
189
424
|
* Generate the connector controller code
|
190
425
|
*/
|
@@ -197,13 +432,26 @@ export class OpenAPIToConnector {
|
|
197
432
|
.map((operation) => {
|
198
433
|
const methodName = this.generateMethodName(operation);
|
199
434
|
const jsdoc = this.generateJSDoc(operation);
|
200
|
-
|
435
|
+
const signature = this.generateMethodSignature(operation);
|
436
|
+
const implementation = this.generateMethodImplementation(operation);
|
437
|
+
return ` /**\n${jsdoc}\n */\n async ${methodName}${signature} {\n${implementation}\n }`;
|
201
438
|
})
|
202
439
|
.join('\n\n');
|
440
|
+
// 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';
|
442
|
+
const startMethod = ` private api: any;
|
443
|
+
|
444
|
+
protected async start(): Promise<void> {
|
445
|
+
this.api = this.getClient({
|
446
|
+
baseUrl: '${baseUrl}',
|
447
|
+
});
|
448
|
+
}`;
|
203
449
|
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
204
450
|
|
205
451
|
export default class Controller extends AbstractController {
|
206
452
|
|
453
|
+
${startMethod}
|
454
|
+
|
207
455
|
${methods}
|
208
456
|
}`;
|
209
457
|
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{
|
2
|
+
"openapi": "3.0.0",
|
3
|
+
"info": {
|
4
|
+
"title": "Test API",
|
5
|
+
"version": "1.0.0",
|
6
|
+
"description": "API without servers definition"
|
7
|
+
},
|
8
|
+
"paths": {
|
9
|
+
"/users": {
|
10
|
+
"get": {
|
11
|
+
"summary": "List users",
|
12
|
+
"operationId": "listUsers",
|
13
|
+
"parameters": [
|
14
|
+
{
|
15
|
+
"name": "limit",
|
16
|
+
"in": "query",
|
17
|
+
"schema": {
|
18
|
+
"type": "integer"
|
19
|
+
},
|
20
|
+
"description": "Maximum number of users"
|
21
|
+
}
|
22
|
+
],
|
23
|
+
"responses": {
|
24
|
+
"200": {
|
25
|
+
"description": "Success"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|