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