@aloma.io/integration-sdk 3.8.59 → 3.8.61
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/build/openapi-to-connector.d.mts +5 -0
- package/build/openapi-to-connector.mjs +61 -4
- package/build/transform/index.mjs +10 -3
- package/package.json +1 -1
- package/src/openapi-to-connector.mts +70 -4
- package/src/transform/index.mts +11 -3
- package/test/openapi-generator-bugs.test.mjs +248 -0
- package/test/scenarios/complex/expected/orders-resource.mts +3 -5
- package/test/scenarios/complex/expected/products-resource.mts +3 -5
|
@@ -30,6 +30,11 @@ export declare class OpenAPIToConnector {
|
|
|
30
30
|
* Generate method name from operation info
|
|
31
31
|
*/
|
|
32
32
|
private generateMethodName;
|
|
33
|
+
/**
|
|
34
|
+
* Disambiguate a colliding method name by prepending path context.
|
|
35
|
+
* E.g., "archive" from path "/contacts/batch/archive" becomes "batchArchive".
|
|
36
|
+
*/
|
|
37
|
+
private disambiguateMethodName;
|
|
33
38
|
/**
|
|
34
39
|
* Get the number of operations in the OpenAPI spec
|
|
35
40
|
*/
|
|
@@ -48,9 +48,21 @@ export class OpenAPIToConnector {
|
|
|
48
48
|
.filter((p) => !VERSION_RE.test(p))
|
|
49
49
|
.filter((p) => !STRIP.has(p));
|
|
50
50
|
// Extract the leaf action from operationId suffix
|
|
51
|
-
|
|
51
|
+
let suffix = operationId.includes('_')
|
|
52
52
|
? operationId.split('_').pop()
|
|
53
53
|
: httpMethod.toLowerCase();
|
|
54
|
+
// Bug 3 fix: If the suffix looks like a raw URL path (contains slashes),
|
|
55
|
+
// clean it by extracting meaningful segments rather than using it literally
|
|
56
|
+
if (suffix.includes('/')) {
|
|
57
|
+
const suffixParts = suffix
|
|
58
|
+
.replace(/\{[^}]+\}/g, '')
|
|
59
|
+
.split('/')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.filter((p) => !VERSION_RE.test(p))
|
|
62
|
+
.filter((p) => !STRIP.has(p));
|
|
63
|
+
// Use the last meaningful segment from the URL-like suffix, or fall back to HTTP method
|
|
64
|
+
suffix = suffixParts.length > 0 ? suffixParts[suffixParts.length - 1] : httpMethod.toLowerCase();
|
|
65
|
+
}
|
|
54
66
|
// Dedup: if suffix equals last path segment, don't repeat it
|
|
55
67
|
const last = parts[parts.length - 1];
|
|
56
68
|
if (suffix === last)
|
|
@@ -167,6 +179,26 @@ export class OpenAPIToConnector {
|
|
|
167
179
|
const pathSuffix = pathParts.join('_') || 'root';
|
|
168
180
|
return `${methodPrefix}_${pathSuffix}`;
|
|
169
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Disambiguate a colliding method name by prepending path context.
|
|
184
|
+
* E.g., "archive" from path "/contacts/batch/archive" becomes "batchArchive".
|
|
185
|
+
*/
|
|
186
|
+
disambiguateMethodName(baseName, operation) {
|
|
187
|
+
// Extract meaningful path segments excluding parameters and the base name itself
|
|
188
|
+
const pathParts = operation.path
|
|
189
|
+
.replace(/\{[^}]+\}/g, '')
|
|
190
|
+
.split('/')
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.filter((p) => p.toLowerCase() !== baseName.toLowerCase());
|
|
193
|
+
// Use the last path segment before the method name as a distinguishing prefix
|
|
194
|
+
if (pathParts.length > 0) {
|
|
195
|
+
const prefix = pathParts[pathParts.length - 1];
|
|
196
|
+
const capitalizedBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
197
|
+
return `${prefix}${capitalizedBase}`;
|
|
198
|
+
}
|
|
199
|
+
// Fallback: prepend the HTTP method
|
|
200
|
+
return `${operation.method.toLowerCase()}${baseName.charAt(0).toUpperCase() + baseName.slice(1)}`;
|
|
201
|
+
}
|
|
170
202
|
/**
|
|
171
203
|
* Get the number of operations in the OpenAPI spec
|
|
172
204
|
*/
|
|
@@ -727,9 +759,9 @@ export class OpenAPIToConnector {
|
|
|
727
759
|
}
|
|
728
760
|
lines.push('');
|
|
729
761
|
}
|
|
762
|
+
// Make the API call
|
|
763
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
730
764
|
}
|
|
731
|
-
// Make the API call
|
|
732
|
-
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
733
765
|
return lines.join('\n');
|
|
734
766
|
}
|
|
735
767
|
/**
|
|
@@ -1175,9 +1207,22 @@ ${jsdoc}
|
|
|
1175
1207
|
throw new Error('No operations found in OpenAPI specification');
|
|
1176
1208
|
}
|
|
1177
1209
|
const resourceName = className.replace('Resource', '').toLowerCase();
|
|
1210
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1211
|
+
const usedNames = new Map();
|
|
1178
1212
|
const functions = operations
|
|
1179
1213
|
.map((operation) => {
|
|
1180
|
-
|
|
1214
|
+
let methodName = this.generateMethodName(operation);
|
|
1215
|
+
// Disambiguate duplicate names by prepending path context
|
|
1216
|
+
if (usedNames.has(methodName)) {
|
|
1217
|
+
methodName = this.disambiguateMethodName(methodName, operation);
|
|
1218
|
+
// If still colliding after disambiguation, append a numeric suffix
|
|
1219
|
+
if (usedNames.has(methodName)) {
|
|
1220
|
+
const count = (usedNames.get(methodName) || 1) + 1;
|
|
1221
|
+
usedNames.set(methodName, count);
|
|
1222
|
+
methodName = `${methodName}${count}`;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
usedNames.set(methodName, (usedNames.get(methodName) || 0) + 1);
|
|
1181
1226
|
const jsdoc = this.generateDetailedJSDoc(operation);
|
|
1182
1227
|
const signature = this.generateMethodSignature(operation);
|
|
1183
1228
|
const implementation = this.generateResourceFunctionImplementation(operation);
|
|
@@ -1261,6 +1306,8 @@ ${exposedMethods}
|
|
|
1261
1306
|
if (operations.length === 0) {
|
|
1262
1307
|
throw new Error('No operations found in OpenAPI specification');
|
|
1263
1308
|
}
|
|
1309
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1310
|
+
const usedNames = new Map();
|
|
1264
1311
|
const methods = operations
|
|
1265
1312
|
.map((operation) => {
|
|
1266
1313
|
let methodName;
|
|
@@ -1270,6 +1317,16 @@ ${exposedMethods}
|
|
|
1270
1317
|
else {
|
|
1271
1318
|
methodName = this.generateMethodName(operation);
|
|
1272
1319
|
}
|
|
1320
|
+
// Disambiguate duplicate names
|
|
1321
|
+
if (usedNames.has(methodName)) {
|
|
1322
|
+
methodName = this.disambiguateMethodName(methodName, operation);
|
|
1323
|
+
if (usedNames.has(methodName)) {
|
|
1324
|
+
const count = (usedNames.get(methodName) || 1) + 1;
|
|
1325
|
+
usedNames.set(methodName, count);
|
|
1326
|
+
methodName = `${methodName}${count}`;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
usedNames.set(methodName, (usedNames.get(methodName) || 0) + 1);
|
|
1273
1330
|
const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
|
|
1274
1331
|
const signature = this.generateMethodSignature(operation);
|
|
1275
1332
|
const implementation = this.generateControllerMethodImplementation(operation); // Use improved implementation
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { parseFromFiles } from '@ts-ast-parser/core';
|
|
2
|
+
function stripQuotes(name) {
|
|
3
|
+
if ((name.startsWith("'") && name.endsWith("'")) || (name.startsWith('"') && name.endsWith('"'))) {
|
|
4
|
+
return name.slice(1, -1);
|
|
5
|
+
}
|
|
6
|
+
return name;
|
|
7
|
+
}
|
|
2
8
|
const transform = (meta) => {
|
|
3
9
|
if (!meta?.length)
|
|
4
10
|
throw new Error('metadata is empty');
|
|
@@ -13,11 +19,12 @@ const transform = (meta) => {
|
|
|
13
19
|
member.isInherited() ||
|
|
14
20
|
member.getKind() !== 'Method' ||
|
|
15
21
|
member.getModifier() !== 'public' ||
|
|
16
|
-
member.getName().startsWith('_'));
|
|
22
|
+
stripQuotes(member.getName()).startsWith('_'));
|
|
17
23
|
});
|
|
18
24
|
const text = members
|
|
19
25
|
.map((member) => {
|
|
20
|
-
|
|
26
|
+
const methodName = stripQuotes(member.getName());
|
|
27
|
+
methods[methodName] = true;
|
|
21
28
|
return member
|
|
22
29
|
.getSignatures()
|
|
23
30
|
.map((sig) => {
|
|
@@ -66,7 +73,7 @@ const transform = (meta) => {
|
|
|
66
73
|
* ${space || ''}
|
|
67
74
|
* ${eg || ''}
|
|
68
75
|
**/
|
|
69
|
-
declare function ${
|
|
76
|
+
declare function ${methodName}(${params}): ${retVal};
|
|
70
77
|
`;
|
|
71
78
|
})
|
|
72
79
|
.join('\n');
|
package/package.json
CHANGED
|
@@ -74,10 +74,23 @@ export class OpenAPIToConnector {
|
|
|
74
74
|
.filter((p) => !STRIP.has(p));
|
|
75
75
|
|
|
76
76
|
// Extract the leaf action from operationId suffix
|
|
77
|
-
|
|
77
|
+
let suffix = operationId.includes('_')
|
|
78
78
|
? operationId.split('_').pop()!
|
|
79
79
|
: httpMethod.toLowerCase();
|
|
80
80
|
|
|
81
|
+
// Bug 3 fix: If the suffix looks like a raw URL path (contains slashes),
|
|
82
|
+
// clean it by extracting meaningful segments rather than using it literally
|
|
83
|
+
if (suffix.includes('/')) {
|
|
84
|
+
const suffixParts = suffix
|
|
85
|
+
.replace(/\{[^}]+\}/g, '')
|
|
86
|
+
.split('/')
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.filter((p) => !VERSION_RE.test(p))
|
|
89
|
+
.filter((p) => !STRIP.has(p));
|
|
90
|
+
// Use the last meaningful segment from the URL-like suffix, or fall back to HTTP method
|
|
91
|
+
suffix = suffixParts.length > 0 ? suffixParts[suffixParts.length - 1] : httpMethod.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
// Dedup: if suffix equals last path segment, don't repeat it
|
|
82
95
|
const last = parts[parts.length - 1];
|
|
83
96
|
if (suffix === last) return parts.join('.');
|
|
@@ -210,6 +223,29 @@ export class OpenAPIToConnector {
|
|
|
210
223
|
return `${methodPrefix}_${pathSuffix}`;
|
|
211
224
|
}
|
|
212
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Disambiguate a colliding method name by prepending path context.
|
|
228
|
+
* E.g., "archive" from path "/contacts/batch/archive" becomes "batchArchive".
|
|
229
|
+
*/
|
|
230
|
+
private disambiguateMethodName(baseName: string, operation: OperationInfo): string {
|
|
231
|
+
// Extract meaningful path segments excluding parameters and the base name itself
|
|
232
|
+
const pathParts = operation.path
|
|
233
|
+
.replace(/\{[^}]+\}/g, '')
|
|
234
|
+
.split('/')
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.filter((p) => p.toLowerCase() !== baseName.toLowerCase());
|
|
237
|
+
|
|
238
|
+
// Use the last path segment before the method name as a distinguishing prefix
|
|
239
|
+
if (pathParts.length > 0) {
|
|
240
|
+
const prefix = pathParts[pathParts.length - 1];
|
|
241
|
+
const capitalizedBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
242
|
+
return `${prefix}${capitalizedBase}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fallback: prepend the HTTP method
|
|
246
|
+
return `${operation.method.toLowerCase()}${baseName.charAt(0).toUpperCase() + baseName.slice(1)}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
/**
|
|
214
250
|
* Get the number of operations in the OpenAPI spec
|
|
215
251
|
*/
|
|
@@ -837,9 +873,10 @@ export class OpenAPIToConnector {
|
|
|
837
873
|
}
|
|
838
874
|
lines.push('');
|
|
839
875
|
}
|
|
876
|
+
|
|
877
|
+
// Make the API call
|
|
878
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
840
879
|
}
|
|
841
|
-
// Make the API call
|
|
842
|
-
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
843
880
|
|
|
844
881
|
return lines.join('\n');
|
|
845
882
|
}
|
|
@@ -1358,9 +1395,24 @@ ${jsdoc}
|
|
|
1358
1395
|
|
|
1359
1396
|
const resourceName = className.replace('Resource', '').toLowerCase();
|
|
1360
1397
|
|
|
1398
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1399
|
+
const usedNames = new Map<string, number>();
|
|
1361
1400
|
const functions = operations
|
|
1362
1401
|
.map((operation) => {
|
|
1363
|
-
|
|
1402
|
+
let methodName = this.generateMethodName(operation);
|
|
1403
|
+
|
|
1404
|
+
// Disambiguate duplicate names by prepending path context
|
|
1405
|
+
if (usedNames.has(methodName)) {
|
|
1406
|
+
methodName = this.disambiguateMethodName(methodName, operation);
|
|
1407
|
+
// If still colliding after disambiguation, append a numeric suffix
|
|
1408
|
+
if (usedNames.has(methodName)) {
|
|
1409
|
+
const count = (usedNames.get(methodName) || 1) + 1;
|
|
1410
|
+
usedNames.set(methodName, count);
|
|
1411
|
+
methodName = `${methodName}${count}`;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
usedNames.set(methodName, (usedNames.get(methodName) || 0) + 1);
|
|
1415
|
+
|
|
1364
1416
|
const jsdoc = this.generateDetailedJSDoc(operation);
|
|
1365
1417
|
const signature = this.generateMethodSignature(operation);
|
|
1366
1418
|
const implementation = this.generateResourceFunctionImplementation(operation);
|
|
@@ -1460,6 +1512,8 @@ ${exposedMethods}
|
|
|
1460
1512
|
throw new Error('No operations found in OpenAPI specification');
|
|
1461
1513
|
}
|
|
1462
1514
|
|
|
1515
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1516
|
+
const usedNames = new Map<string, number>();
|
|
1463
1517
|
const methods = operations
|
|
1464
1518
|
.map((operation) => {
|
|
1465
1519
|
let methodName: string;
|
|
@@ -1468,6 +1522,18 @@ ${exposedMethods}
|
|
|
1468
1522
|
} else {
|
|
1469
1523
|
methodName = this.generateMethodName(operation);
|
|
1470
1524
|
}
|
|
1525
|
+
|
|
1526
|
+
// Disambiguate duplicate names
|
|
1527
|
+
if (usedNames.has(methodName)) {
|
|
1528
|
+
methodName = this.disambiguateMethodName(methodName, operation);
|
|
1529
|
+
if (usedNames.has(methodName)) {
|
|
1530
|
+
const count = (usedNames.get(methodName) || 1) + 1;
|
|
1531
|
+
usedNames.set(methodName, count);
|
|
1532
|
+
methodName = `${methodName}${count}`;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
usedNames.set(methodName, (usedNames.get(methodName) || 0) + 1);
|
|
1536
|
+
|
|
1471
1537
|
const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
|
|
1472
1538
|
const signature = this.generateMethodSignature(operation);
|
|
1473
1539
|
const implementation = this.generateControllerMethodImplementation(operation); // Use improved implementation
|
package/src/transform/index.mts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import {parseFromFiles} from '@ts-ast-parser/core';
|
|
2
2
|
|
|
3
|
+
function stripQuotes(name: string): string {
|
|
4
|
+
if ((name.startsWith("'") && name.endsWith("'")) || (name.startsWith('"') && name.endsWith('"'))) {
|
|
5
|
+
return name.slice(1, -1);
|
|
6
|
+
}
|
|
7
|
+
return name;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
const transform = (meta: any) => {
|
|
4
11
|
if (!meta?.length) throw new Error('metadata is empty');
|
|
5
12
|
meta = meta[0];
|
|
@@ -17,13 +24,14 @@ const transform = (meta: any) => {
|
|
|
17
24
|
member.isInherited() ||
|
|
18
25
|
member.getKind() !== 'Method' ||
|
|
19
26
|
member.getModifier() !== 'public' ||
|
|
20
|
-
member.getName().startsWith('_')
|
|
27
|
+
stripQuotes(member.getName()).startsWith('_')
|
|
21
28
|
);
|
|
22
29
|
});
|
|
23
30
|
|
|
24
31
|
const text = members
|
|
25
32
|
.map((member: any) => {
|
|
26
|
-
|
|
33
|
+
const methodName = stripQuotes(member.getName());
|
|
34
|
+
methods[methodName] = true;
|
|
27
35
|
|
|
28
36
|
return member
|
|
29
37
|
.getSignatures()
|
|
@@ -85,7 +93,7 @@ const transform = (meta: any) => {
|
|
|
85
93
|
* ${space || ''}
|
|
86
94
|
* ${eg || ''}
|
|
87
95
|
**/
|
|
88
|
-
declare function ${
|
|
96
|
+
declare function ${methodName}(${params}): ${retVal};
|
|
89
97
|
`;
|
|
90
98
|
})
|
|
91
99
|
.join('\n');
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { OpenAPIToConnector } from '../build/openapi-to-connector.mjs';
|
|
2
|
+
|
|
3
|
+
// Simple test framework matching project convention
|
|
4
|
+
const colors = {
|
|
5
|
+
green: '\x1b[32m',
|
|
6
|
+
red: '\x1b[31m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
cyan: '\x1b[36m'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
async function test(name, fn) {
|
|
16
|
+
try {
|
|
17
|
+
console.log(`${colors.cyan}Running: ${name}${colors.reset}`);
|
|
18
|
+
await fn();
|
|
19
|
+
console.log(`${colors.green}✓ PASS: ${name}${colors.reset}\n`);
|
|
20
|
+
passed++;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.log(`${colors.red}✗ FAIL: ${name}${colors.reset}`);
|
|
23
|
+
console.log(`${colors.red} Error: ${error.message}${colors.reset}\n`);
|
|
24
|
+
failed++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assert(condition, message) {
|
|
29
|
+
if (!condition) throw new Error(message || 'Assertion failed');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertEqual(actual, expected, message) {
|
|
33
|
+
if (actual !== expected) {
|
|
34
|
+
throw new Error(message || `Expected "${expected}", got "${actual}"`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Minimal OpenAPI spec for testing
|
|
39
|
+
const minimalSpec = {
|
|
40
|
+
openapi: '3.0.0',
|
|
41
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
42
|
+
paths: {}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// === BUG 1: Duplicate function names within a resource ===
|
|
46
|
+
|
|
47
|
+
await test('Bug1: Two ops producing same method name are deduplicated in resource output', async () => {
|
|
48
|
+
// Spec with two operations that both derive "archive" as method name:
|
|
49
|
+
// POST /batch/archive -> operationId batch_archive (suffix: archive)
|
|
50
|
+
// DELETE /contacts/{id} -> operationId contacts_archive (suffix: archive)
|
|
51
|
+
const spec = {
|
|
52
|
+
openapi: '3.0.0',
|
|
53
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
54
|
+
paths: {
|
|
55
|
+
'/contacts/batch/archive': {
|
|
56
|
+
post: {
|
|
57
|
+
operationId: 'batch_archive',
|
|
58
|
+
summary: 'Batch archive contacts',
|
|
59
|
+
responses: { '200': { description: 'OK' } }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
'/contacts/{contactId}': {
|
|
63
|
+
delete: {
|
|
64
|
+
operationId: 'contacts_archive',
|
|
65
|
+
summary: 'Archive single contact',
|
|
66
|
+
parameters: [{ name: 'contactId', in: 'path', required: true, schema: { type: 'string' } }],
|
|
67
|
+
responses: { '204': { description: 'Deleted' } }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const generator = new OpenAPIToConnector(spec, 'test', { nestedPaths: false });
|
|
74
|
+
const output = generator.generateResourceClass('ContactsResource');
|
|
75
|
+
|
|
76
|
+
// Count how many times "export function archive(" appears
|
|
77
|
+
const matches = output.match(/export function archive\(/g);
|
|
78
|
+
assert(
|
|
79
|
+
!matches || matches.length <= 1,
|
|
80
|
+
`Expected at most 1 "export function archive(" but found ${matches ? matches.length : 0}. Duplicate function names!`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// There should be two distinct function declarations (disambiguated)
|
|
84
|
+
const funcDecls = output.match(/export function \w+\(/g);
|
|
85
|
+
assert(funcDecls && funcDecls.length >= 2, `Expected at least 2 distinct function declarations, got ${funcDecls ? funcDecls.length : 0}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await test('Bug1: generateController with colliding method names produces distinct methods', async () => {
|
|
89
|
+
const spec = {
|
|
90
|
+
openapi: '3.0.0',
|
|
91
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
92
|
+
paths: {
|
|
93
|
+
'/items/batch/read': {
|
|
94
|
+
post: {
|
|
95
|
+
operationId: 'batch_read',
|
|
96
|
+
summary: 'Batch read items',
|
|
97
|
+
responses: { '200': { description: 'OK' } }
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
'/items/{itemId}': {
|
|
101
|
+
get: {
|
|
102
|
+
operationId: 'items_read',
|
|
103
|
+
summary: 'Read single item',
|
|
104
|
+
parameters: [{ name: 'itemId', in: 'path', required: true, schema: { type: 'string' } }],
|
|
105
|
+
responses: { '200': { description: 'OK' } }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const generator = new OpenAPIToConnector(spec, 'test', { nestedPaths: false });
|
|
112
|
+
const output = generator.generateController();
|
|
113
|
+
|
|
114
|
+
// Count occurrences of "async read(" — should be at most 1
|
|
115
|
+
const matches = output.match(/async read\(/g);
|
|
116
|
+
assert(
|
|
117
|
+
!matches || matches.length <= 1,
|
|
118
|
+
`Expected at most 1 "async read(" but found ${matches ? matches.length : 0}. Duplicate method names in controller!`
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// === BUG 2: Dead code after return ===
|
|
123
|
+
|
|
124
|
+
await test('Bug2: Resource function for simple path+params has no statements after return', async () => {
|
|
125
|
+
// A simple GET with path param and no query/body triggers the "isSimple" branch
|
|
126
|
+
// which already emits a return, but then the code unconditionally appends another return
|
|
127
|
+
const spec = {
|
|
128
|
+
openapi: '3.0.0',
|
|
129
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
130
|
+
paths: {
|
|
131
|
+
'/contacts/{contactId}': {
|
|
132
|
+
get: {
|
|
133
|
+
operationId: 'getContact',
|
|
134
|
+
summary: 'Get a contact',
|
|
135
|
+
parameters: [{ name: 'contactId', in: 'path', required: true, schema: { type: 'string' } }],
|
|
136
|
+
responses: { '200': { description: 'OK' } }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const generator = new OpenAPIToConnector(spec, 'test', { nestedPaths: false });
|
|
143
|
+
const output = generator.generateResourceClass('ContactsResource');
|
|
144
|
+
|
|
145
|
+
// Extract the function body for getContact
|
|
146
|
+
const funcMatch = output.match(/export function getContact[\s\S]*?\n\}/);
|
|
147
|
+
assert(funcMatch, 'Could not find getContact function in output');
|
|
148
|
+
const funcBody = funcMatch[0];
|
|
149
|
+
|
|
150
|
+
// Find all return statements
|
|
151
|
+
const returnStatements = funcBody.match(/return this\.api\.fetch/g);
|
|
152
|
+
assert(
|
|
153
|
+
returnStatements && returnStatements.length === 1,
|
|
154
|
+
`Expected exactly 1 return statement but found ${returnStatements ? returnStatements.length : 0}. Dead code after return!`
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await test('Bug2: Controller function for simple path+params has no dead code after return', async () => {
|
|
159
|
+
const spec = {
|
|
160
|
+
openapi: '3.0.0',
|
|
161
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
162
|
+
paths: {
|
|
163
|
+
'/items/{itemId}': {
|
|
164
|
+
get: {
|
|
165
|
+
operationId: 'getItem',
|
|
166
|
+
summary: 'Get an item',
|
|
167
|
+
parameters: [{ name: 'itemId', in: 'path', required: true, schema: { type: 'string' } }],
|
|
168
|
+
responses: { '200': { description: 'OK' } }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const generator = new OpenAPIToConnector(spec, 'test', { nestedPaths: false });
|
|
175
|
+
const output = generator.generateController();
|
|
176
|
+
|
|
177
|
+
// Extract the method body for getItem
|
|
178
|
+
const methodMatch = output.match(/async getItem[\s\S]*?\n \}/);
|
|
179
|
+
assert(methodMatch, 'Could not find getItem method in output');
|
|
180
|
+
const methodBody = methodMatch[0];
|
|
181
|
+
|
|
182
|
+
// Find all return statements
|
|
183
|
+
const returnStatements = methodBody.match(/return this\.api\.fetch/g);
|
|
184
|
+
assert(
|
|
185
|
+
returnStatements && returnStatements.length === 1,
|
|
186
|
+
`Expected exactly 1 return statement but found ${returnStatements ? returnStatements.length : 0}. Dead code after return in controller!`
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// === BUG 3: Raw URL path segments in method names ===
|
|
191
|
+
|
|
192
|
+
await test('Bug3: deriveMethodPath handles operationId with raw URL paths (lists style)', async () => {
|
|
193
|
+
const generator = new OpenAPIToConnector(minimalSpec, 'hubspot', { nestedPaths: true });
|
|
194
|
+
|
|
195
|
+
// HubSpot lists-style operationId: "get-/crm/v3/lists_/crm/v3/lists"
|
|
196
|
+
const result = generator.deriveMethodPath('GET', '/crm/v3/lists', 'get-/crm/v3/lists_/crm/v3/lists');
|
|
197
|
+
|
|
198
|
+
// Should NOT contain slashes or version numbers
|
|
199
|
+
assert(!result.includes('/'), `Method path should not contain slashes, got: "${result}"`);
|
|
200
|
+
assert(!result.includes('v3'), `Method path should not contain version numbers, got: "${result}"`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await test('Bug3: deriveMethodPath handles operationId suffix that IS a raw URL path', async () => {
|
|
204
|
+
const generator = new OpenAPIToConnector(minimalSpec, 'hubspot', { nestedPaths: true });
|
|
205
|
+
|
|
206
|
+
// operationId: "post-/crm/v3/lists/search_/crm/v3/lists/search"
|
|
207
|
+
const result = generator.deriveMethodPath('POST', '/crm/v3/lists/search', 'post-/crm/v3/lists/search_/crm/v3/lists/search');
|
|
208
|
+
|
|
209
|
+
assert(!result.includes('/'), `Method path should not contain slashes, got: "${result}"`);
|
|
210
|
+
assert(!result.includes('v3'), `Method path should not contain version numbers, got: "${result}"`);
|
|
211
|
+
// Should contain "lists" and "search" in some form
|
|
212
|
+
assert(result.includes('lists'), `Method path should contain "lists", got: "${result}"`);
|
|
213
|
+
assert(result.includes('search'), `Method path should contain "search", got: "${result}"`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await test('Bug3: deriveMethodPath does not produce doubled segments from URL-style operationId', async () => {
|
|
217
|
+
const generator = new OpenAPIToConnector(minimalSpec, 'hubspot', { nestedPaths: true });
|
|
218
|
+
|
|
219
|
+
// operationId: "put-/crm/v3/lists/{listId}/restore_/crm/v3/lists/{listId}/restore"
|
|
220
|
+
const result = generator.deriveMethodPath('PUT', '/crm/v3/lists/{listId}/restore', 'put-/crm/v3/lists/{listId}/restore_/crm/v3/lists/{listId}/restore');
|
|
221
|
+
|
|
222
|
+
assert(!result.includes('/'), `Method path should not contain slashes, got: "${result}"`);
|
|
223
|
+
// Should not have doubled path segments like "lists.lists" or "restore.restore"
|
|
224
|
+
const parts = result.split('.');
|
|
225
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
226
|
+
assert(parts[i] !== parts[i + 1], `Doubled segment "${parts[i]}" in method path: "${result}"`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await test('Bug3: generateMethodName (flat mode) cleans operationId with raw URL paths', async () => {
|
|
231
|
+
const generator = new OpenAPIToConnector(minimalSpec, 'hubspot', { nestedPaths: false });
|
|
232
|
+
|
|
233
|
+
// When nestedPaths is false, deriveMethodPath calls generateMethodName
|
|
234
|
+
// This should still produce a clean identifier without slashes
|
|
235
|
+
const result = generator.deriveMethodPath('GET', '/crm/v3/lists', 'get-/crm/v3/lists_/crm/v3/lists');
|
|
236
|
+
|
|
237
|
+
assert(!result.includes('/'), `Flat method name should not contain slashes, got: "${result}"`);
|
|
238
|
+
assert(/^[a-zA-Z_]/.test(result), `Method name should start with letter or underscore, got: "${result}"`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// --- Report ---
|
|
242
|
+
console.log(`\n${colors.yellow}===== Results =====${colors.reset}`);
|
|
243
|
+
console.log(`${colors.green}Passed: ${passed}${colors.reset}`);
|
|
244
|
+
console.log(`${colors.red}Failed: ${failed}${colors.reset}`);
|
|
245
|
+
|
|
246
|
+
if (failed > 0) {
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
@@ -117,7 +117,7 @@ export function getOrders(this: any, options?: {status?: string, customerId?: st
|
|
|
117
117
|
fetchOptions.params.limit = options.limit;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
return this.api.fetch(url, fetchOptions);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
@@ -156,7 +156,7 @@ export function createOrder(this: any, options: {customerId: string /** Customer
|
|
|
156
156
|
headers: options.headers,
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
return this.api.fetch(url, fetchOptions);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
@@ -187,7 +187,6 @@ export function getOrder(this: any, orderId: string) {
|
|
|
187
187
|
return this.api.fetch(url, {
|
|
188
188
|
method: 'GET',
|
|
189
189
|
});
|
|
190
|
-
return this.api.fetch(url, fetchOptions);
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
/**
|
|
@@ -229,7 +228,7 @@ export function updateOrderStatus(this: any, orderId: string, options: {status:
|
|
|
229
228
|
headers: options.headers,
|
|
230
229
|
};
|
|
231
230
|
|
|
232
|
-
|
|
231
|
+
return this.api.fetch(url, fetchOptions);
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
/**
|
|
@@ -260,5 +259,4 @@ export function cancelOrder(this: any, orderId: string) {
|
|
|
260
259
|
return this.api.fetch(url, {
|
|
261
260
|
method: 'POST',
|
|
262
261
|
});
|
|
263
|
-
return this.api.fetch(url, fetchOptions);
|
|
264
262
|
}
|
|
@@ -98,7 +98,7 @@ export function getProducts(this: any, options?: {limit?: number, category?: str
|
|
|
98
98
|
fetchOptions.params.archived = options.archived;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
return this.api.fetch(url, fetchOptions);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -138,7 +138,7 @@ export function createProduct(this: any, options: {name: string /** Product name
|
|
|
138
138
|
headers: options.headers,
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
return this.api.fetch(url, fetchOptions);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
@@ -169,7 +169,6 @@ export function getProduct(this: any, productId: string) {
|
|
|
169
169
|
return this.api.fetch(url, {
|
|
170
170
|
method: 'GET',
|
|
171
171
|
});
|
|
172
|
-
return this.api.fetch(url, fetchOptions);
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
/**
|
|
@@ -215,7 +214,7 @@ export function updateProduct(this: any, productId: string, options: {name: stri
|
|
|
215
214
|
headers: options.headers,
|
|
216
215
|
};
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
return this.api.fetch(url, fetchOptions);
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
/**
|
|
@@ -235,5 +234,4 @@ export function deleteProduct(this: any, productId: string) {
|
|
|
235
234
|
return this.api.fetch(url, {
|
|
236
235
|
method: 'DELETE',
|
|
237
236
|
});
|
|
238
|
-
return this.api.fetch(url, fetchOptions);
|
|
239
237
|
}
|