@aloma.io/integration-sdk 3.8.57 → 3.8.60
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/builder/runtime-context.d.mts +11 -0
- package/build/builder/runtime-context.mjs +36 -8
- package/build/cli.mjs +11 -5
- package/build/openapi-to-connector.d.mts +16 -1
- package/build/openapi-to-connector.mjs +115 -9
- package/package.json +1 -1
- package/src/builder/runtime-context.mts +38 -9
- package/src/cli.mts +11 -5
- package/src/openapi-to-connector.mts +135 -9
- package/test/nested-resolver.test.mjs +184 -0
- package/test/openapi-generator-bugs.test.mjs +248 -0
- package/test/openapi-nested-paths.test.mjs +100 -0
- package/test/scenarios/complex/expected/orders-resource.mts +3 -5
- package/test/scenarios/complex/expected/products-resource.mts +3 -5
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { AbstractController } from '../controller/index.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* Build a resolvers object from a list of method names and a controller.
|
|
4
|
+
*
|
|
5
|
+
* If a method name contains dots (e.g. 'crm.contacts.create'), it is registered
|
|
6
|
+
* as a nested object tree so that resolveMethod(['crm', 'contacts', 'create'])
|
|
7
|
+
* finds the handler.
|
|
8
|
+
*
|
|
9
|
+
* If a method name has no dots (e.g. 'contactsCreate'), it is registered flat
|
|
10
|
+
* as before: resolvers.contactsCreate = handler.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildResolvers(methods: string[], controller: any): any;
|
|
2
13
|
/**
|
|
3
14
|
* Runtime context to manage the lifecycle of the connector
|
|
4
15
|
*/
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { AbstractController } from '../controller/index.mjs';
|
|
3
3
|
import { Connector } from '../internal/index.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* Build a resolvers object from a list of method names and a controller.
|
|
6
|
+
*
|
|
7
|
+
* If a method name contains dots (e.g. 'crm.contacts.create'), it is registered
|
|
8
|
+
* as a nested object tree so that resolveMethod(['crm', 'contacts', 'create'])
|
|
9
|
+
* finds the handler.
|
|
10
|
+
*
|
|
11
|
+
* If a method name has no dots (e.g. 'contactsCreate'), it is registered flat
|
|
12
|
+
* as before: resolvers.contactsCreate = handler.
|
|
13
|
+
*/
|
|
14
|
+
export function buildResolvers(methods, controller) {
|
|
15
|
+
const resolvers = {};
|
|
16
|
+
methods.forEach((method) => {
|
|
17
|
+
const handler = async (args) => {
|
|
18
|
+
if (!methods.includes(method))
|
|
19
|
+
throw new Error(`${method} not found`);
|
|
20
|
+
return controller[method](args);
|
|
21
|
+
};
|
|
22
|
+
if (method.includes('.')) {
|
|
23
|
+
const parts = method.split('.');
|
|
24
|
+
let node = resolvers;
|
|
25
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
26
|
+
if (!node[parts[i]] || typeof node[parts[i]] !== 'object') {
|
|
27
|
+
node[parts[i]] = {};
|
|
28
|
+
}
|
|
29
|
+
node = node[parts[i]];
|
|
30
|
+
}
|
|
31
|
+
node[parts[parts.length - 1]] = handler;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
resolvers[method] = handler;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return resolvers;
|
|
38
|
+
}
|
|
4
39
|
/**
|
|
5
40
|
* Runtime context to manage the lifecycle of the connector
|
|
6
41
|
*/
|
|
@@ -36,15 +71,8 @@ export default class RuntimeContext {
|
|
|
36
71
|
icon,
|
|
37
72
|
});
|
|
38
73
|
const configuration = connector.configure().config(data.config || {});
|
|
39
|
-
const resolvers = {};
|
|
40
74
|
const methods = [...data.methods, '__autocomplete', '__endpoint', '__default'];
|
|
41
|
-
methods
|
|
42
|
-
resolvers[method] = async (args) => {
|
|
43
|
-
if (!methods.includes(method))
|
|
44
|
-
throw new Error(`${method} not found`);
|
|
45
|
-
return controller[method](args);
|
|
46
|
-
};
|
|
47
|
-
});
|
|
75
|
+
const resolvers = buildResolvers(methods, controller);
|
|
48
76
|
configuration.types(data.types).resolvers(resolvers);
|
|
49
77
|
if (data.options?.endpoint?.enabled) {
|
|
50
78
|
configuration.endpoint((arg) => controller.__endpoint(arg), data.options?.endpoint?.required);
|
package/build/cli.mjs
CHANGED
|
@@ -112,6 +112,7 @@ program
|
|
|
112
112
|
.option('--resource <className>', 'Generate as a resource class with the specified class name (e.g., CompaniesResource)')
|
|
113
113
|
.option('--multi-resource', 'Generate multiple resource files + main controller (requires multiple --spec files)')
|
|
114
114
|
.option('--controller-only', 'Generate only the controller file, do not create full project structure')
|
|
115
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
115
116
|
.option('--no-build', 'Skip installing dependencies and building the project')
|
|
116
117
|
.action(async (name, options) => {
|
|
117
118
|
name = name.replace(/[\/\.]/gi, '');
|
|
@@ -122,7 +123,8 @@ program
|
|
|
122
123
|
const specContent = fs.readFileSync(options.spec, 'utf-8');
|
|
123
124
|
const spec = OpenAPIToConnector.parseSpec(specContent);
|
|
124
125
|
// Generate the controller from OpenAPI spec
|
|
125
|
-
const
|
|
126
|
+
const generatorOptions = { nestedPaths: !!options.nestedPaths };
|
|
127
|
+
const generator = new OpenAPIToConnector(spec, name, generatorOptions);
|
|
126
128
|
let controllerCode;
|
|
127
129
|
if (options.resource) {
|
|
128
130
|
console.log(`Generating resource class '${options.resource}' from OpenAPI specification...`);
|
|
@@ -220,12 +222,14 @@ program
|
|
|
220
222
|
.requiredOption('--connector-id <id>', 'id of the connector')
|
|
221
223
|
.requiredOption('--resources <specs>', 'comma-separated list of "className:specFile" pairs (e.g., "CompaniesResource:companies.json,ContactsResource:contacts.json")')
|
|
222
224
|
.option('--base-url <url>', 'base URL for the API (if not specified, will be extracted from first OpenAPI spec)')
|
|
225
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
223
226
|
.option('--no-build', 'Skip installing dependencies and building the project')
|
|
224
227
|
.action(async (name, options) => {
|
|
225
228
|
name = name.replace(/[\/\.]/gi, '');
|
|
226
229
|
if (!name)
|
|
227
230
|
throw new Error('name is empty');
|
|
228
231
|
const target = `${process.cwd()}/${name}`;
|
|
232
|
+
const generatorOptions = { nestedPaths: !!options.nestedPaths };
|
|
229
233
|
try {
|
|
230
234
|
// Parse resources specification
|
|
231
235
|
const resourceSpecs = options.resources.split(',').map((spec) => {
|
|
@@ -253,7 +257,7 @@ program
|
|
|
253
257
|
baseUrl = spec.servers[0].url;
|
|
254
258
|
}
|
|
255
259
|
// Generate the resource class
|
|
256
|
-
const generator = new OpenAPIToConnector(spec, name);
|
|
260
|
+
const generator = new OpenAPIToConnector(spec, name, generatorOptions);
|
|
257
261
|
const resourceCode = generator.generateResourceClass(className);
|
|
258
262
|
// Write the resource file
|
|
259
263
|
const fileName = className.toLowerCase().replace('resource', '');
|
|
@@ -266,7 +270,7 @@ program
|
|
|
266
270
|
// Generate the main controller
|
|
267
271
|
console.log('Generating main controller...');
|
|
268
272
|
const firstSpec = OpenAPIToConnector.parseSpec(fs.readFileSync(resourceSpecs[0].specFile, 'utf-8'));
|
|
269
|
-
const mainGenerator = new OpenAPIToConnector(firstSpec, name);
|
|
273
|
+
const mainGenerator = new OpenAPIToConnector(firstSpec, name, generatorOptions);
|
|
270
274
|
const mainControllerCode = mainGenerator.generateMainController(resources, parsedResourceSpecs);
|
|
271
275
|
// Write the main controller
|
|
272
276
|
const controllerPath = `${target}/src/controller/index.mts`;
|
|
@@ -310,6 +314,7 @@ program
|
|
|
310
314
|
.argument('<projectPath>', 'path to the existing connector project')
|
|
311
315
|
.requiredOption('--className <name>', 'class name for the resource (e.g., DealsResource)')
|
|
312
316
|
.requiredOption('--spec <file>', 'OpenAPI specification file for the new resource')
|
|
317
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
313
318
|
.option('--no-build', 'Skip building the project after adding the resource')
|
|
314
319
|
.action(async (projectPath, options) => {
|
|
315
320
|
const target = path.resolve(projectPath);
|
|
@@ -326,7 +331,8 @@ program
|
|
|
326
331
|
const specContent = fs.readFileSync(options.spec, 'utf-8');
|
|
327
332
|
const spec = OpenAPIToConnector.parseSpec(specContent);
|
|
328
333
|
// Generate the resource functions file (new function-based pattern)
|
|
329
|
-
const
|
|
334
|
+
const generatorOptions = { nestedPaths: !!options.nestedPaths };
|
|
335
|
+
const generator = new OpenAPIToConnector(spec, 'Resource', generatorOptions);
|
|
330
336
|
const resourceCode = generator.generateResourceClass(options.className);
|
|
331
337
|
// Write the resource file
|
|
332
338
|
const fileName = options.className.toLowerCase().replace('resource', '');
|
|
@@ -384,7 +390,7 @@ program
|
|
|
384
390
|
const resources = [{ className: options.className, fileName }];
|
|
385
391
|
const resourceSpecs = [{ fileName, spec }];
|
|
386
392
|
// Create a temporary generator to generate just the exposed methods for this resource
|
|
387
|
-
const tempGenerator = new OpenAPIToConnector(spec, 'temp');
|
|
393
|
+
const tempGenerator = new OpenAPIToConnector(spec, 'temp', generatorOptions);
|
|
388
394
|
const exposedMethods = tempGenerator.generateExposedResourceMethods(resources, resourceSpecs);
|
|
389
395
|
// Add the exposed methods to the controller before the closing brace
|
|
390
396
|
if (exposedMethods.trim()) {
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
export interface OpenAPIToConnectorOptions {
|
|
4
|
+
nestedPaths?: boolean;
|
|
5
|
+
}
|
|
3
6
|
export declare class OpenAPIToConnector {
|
|
4
7
|
private spec;
|
|
5
8
|
private connectorName;
|
|
6
|
-
|
|
9
|
+
private options;
|
|
10
|
+
constructor(spec: OpenAPIV3.Document, connectorName: string, options?: OpenAPIToConnectorOptions);
|
|
11
|
+
/**
|
|
12
|
+
* Derive a dotted method path from URL path and operationId.
|
|
13
|
+
* When nestedPaths is enabled, builds a namespace from the URL segments.
|
|
14
|
+
* When disabled (default), falls back to the existing flat method name.
|
|
15
|
+
*/
|
|
16
|
+
deriveMethodPath(httpMethod: string, urlPath: string, operationId: string): string;
|
|
7
17
|
/**
|
|
8
18
|
* Parse OpenAPI spec from JSON or YAML string
|
|
9
19
|
*/
|
|
@@ -20,6 +30,11 @@ export declare class OpenAPIToConnector {
|
|
|
20
30
|
* Generate method name from operation info
|
|
21
31
|
*/
|
|
22
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;
|
|
23
38
|
/**
|
|
24
39
|
* Get the number of operations in the OpenAPI spec
|
|
25
40
|
*/
|
|
@@ -23,9 +23,51 @@ const OpenAPISchema = z.object({
|
|
|
23
23
|
export class OpenAPIToConnector {
|
|
24
24
|
spec;
|
|
25
25
|
connectorName;
|
|
26
|
-
|
|
26
|
+
options;
|
|
27
|
+
constructor(spec, connectorName, options) {
|
|
27
28
|
this.spec = spec;
|
|
28
29
|
this.connectorName = connectorName;
|
|
30
|
+
this.options = options || {};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive a dotted method path from URL path and operationId.
|
|
34
|
+
* When nestedPaths is enabled, builds a namespace from the URL segments.
|
|
35
|
+
* When disabled (default), falls back to the existing flat method name.
|
|
36
|
+
*/
|
|
37
|
+
deriveMethodPath(httpMethod, urlPath, operationId) {
|
|
38
|
+
if (!this.options.nestedPaths) {
|
|
39
|
+
return this.generateMethodName({ method: httpMethod, path: urlPath, operationId });
|
|
40
|
+
}
|
|
41
|
+
const VERSION_RE = /^v\d+$/;
|
|
42
|
+
const STRIP = new Set(['objects', 'items']);
|
|
43
|
+
// Build namespace parts from the URL path
|
|
44
|
+
const parts = urlPath
|
|
45
|
+
.replace(/\{[^}]+\}/g, '')
|
|
46
|
+
.split('/')
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.filter((p) => !VERSION_RE.test(p))
|
|
49
|
+
.filter((p) => !STRIP.has(p));
|
|
50
|
+
// Extract the leaf action from operationId suffix
|
|
51
|
+
let suffix = operationId.includes('_')
|
|
52
|
+
? operationId.split('_').pop()
|
|
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
|
+
}
|
|
66
|
+
// Dedup: if suffix equals last path segment, don't repeat it
|
|
67
|
+
const last = parts[parts.length - 1];
|
|
68
|
+
if (suffix === last)
|
|
69
|
+
return parts.join('.');
|
|
70
|
+
return [...parts, suffix].join('.');
|
|
29
71
|
}
|
|
30
72
|
/**
|
|
31
73
|
* Parse OpenAPI spec from JSON or YAML string
|
|
@@ -137,6 +179,26 @@ export class OpenAPIToConnector {
|
|
|
137
179
|
const pathSuffix = pathParts.join('_') || 'root';
|
|
138
180
|
return `${methodPrefix}_${pathSuffix}`;
|
|
139
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
|
+
}
|
|
140
202
|
/**
|
|
141
203
|
* Get the number of operations in the OpenAPI spec
|
|
142
204
|
*/
|
|
@@ -697,9 +759,9 @@ export class OpenAPIToConnector {
|
|
|
697
759
|
}
|
|
698
760
|
lines.push('');
|
|
699
761
|
}
|
|
762
|
+
// Make the API call
|
|
763
|
+
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
700
764
|
}
|
|
701
|
-
// Make the API call
|
|
702
|
-
lines.push(` return this.api.fetch(url, fetchOptions);`);
|
|
703
765
|
return lines.join('\n');
|
|
704
766
|
}
|
|
705
767
|
/**
|
|
@@ -713,20 +775,31 @@ export class OpenAPIToConnector {
|
|
|
713
775
|
const resourceSpec = resourceSpecs?.find((rs) => rs.fileName === resourceName);
|
|
714
776
|
if (resourceSpec) {
|
|
715
777
|
// Create a temporary generator for this resource's spec
|
|
716
|
-
const resourceGenerator = new OpenAPIToConnector(resourceSpec.spec, resourceName);
|
|
778
|
+
const resourceGenerator = new OpenAPIToConnector(resourceSpec.spec, resourceName, this.options);
|
|
717
779
|
const operations = resourceGenerator.extractOperations();
|
|
718
780
|
for (const operation of operations) {
|
|
719
781
|
const methodName = resourceGenerator.generateMethodName(operation);
|
|
720
782
|
const jsdoc = resourceGenerator.generateDetailedJSDoc(operation);
|
|
721
783
|
const signature = resourceGenerator.generateMethodSignature(operation);
|
|
722
784
|
// Generate the exposed method that delegates to the resource
|
|
723
|
-
|
|
785
|
+
let exposedMethodName;
|
|
786
|
+
if (this.options.nestedPaths && operation.operationId) {
|
|
787
|
+
// Use dotted path for nested method names
|
|
788
|
+
exposedMethodName = resourceGenerator.deriveMethodPath(operation.method, operation.path, operation.operationId);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
exposedMethodName = `${resourceName}${methodName.charAt(0).toUpperCase() + methodName.slice(1)}`;
|
|
792
|
+
}
|
|
724
793
|
// Generate parameter call based on operation details
|
|
725
794
|
const parameterCall = this.generateParameterCallForOperation(operation, signature);
|
|
795
|
+
// Use quoted method name if it contains dots
|
|
796
|
+
const methodDecl = exposedMethodName.includes('.')
|
|
797
|
+
? `'${exposedMethodName}'`
|
|
798
|
+
: exposedMethodName;
|
|
726
799
|
methods.push(` /**
|
|
727
800
|
${jsdoc}
|
|
728
801
|
*/
|
|
729
|
-
async ${
|
|
802
|
+
async ${methodDecl}${signature} {
|
|
730
803
|
return this.${resourceName}.${methodName}(${parameterCall});
|
|
731
804
|
}`);
|
|
732
805
|
}
|
|
@@ -1134,9 +1207,22 @@ ${jsdoc}
|
|
|
1134
1207
|
throw new Error('No operations found in OpenAPI specification');
|
|
1135
1208
|
}
|
|
1136
1209
|
const resourceName = className.replace('Resource', '').toLowerCase();
|
|
1210
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1211
|
+
const usedNames = new Map();
|
|
1137
1212
|
const functions = operations
|
|
1138
1213
|
.map((operation) => {
|
|
1139
|
-
|
|
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);
|
|
1140
1226
|
const jsdoc = this.generateDetailedJSDoc(operation);
|
|
1141
1227
|
const signature = this.generateMethodSignature(operation);
|
|
1142
1228
|
const implementation = this.generateResourceFunctionImplementation(operation);
|
|
@@ -1220,13 +1306,33 @@ ${exposedMethods}
|
|
|
1220
1306
|
if (operations.length === 0) {
|
|
1221
1307
|
throw new Error('No operations found in OpenAPI specification');
|
|
1222
1308
|
}
|
|
1309
|
+
// Bug 1 fix: Track used method names and disambiguate collisions
|
|
1310
|
+
const usedNames = new Map();
|
|
1223
1311
|
const methods = operations
|
|
1224
1312
|
.map((operation) => {
|
|
1225
|
-
|
|
1313
|
+
let methodName;
|
|
1314
|
+
if (this.options.nestedPaths && operation.operationId) {
|
|
1315
|
+
methodName = this.deriveMethodPath(operation.method, operation.path, operation.operationId);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
methodName = this.generateMethodName(operation);
|
|
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);
|
|
1226
1330
|
const jsdoc = this.generateDetailedJSDoc(operation); // Use detailed JSDoc like multi-resource
|
|
1227
1331
|
const signature = this.generateMethodSignature(operation);
|
|
1228
1332
|
const implementation = this.generateControllerMethodImplementation(operation); // Use improved implementation
|
|
1229
|
-
|
|
1333
|
+
// Use quoted method name if it contains dots
|
|
1334
|
+
const methodDecl = methodName.includes('.') ? `'${methodName}'` : methodName;
|
|
1335
|
+
return ` /**\n${jsdoc}\n */\n async ${methodDecl}${signature} {\n${implementation}\n }`;
|
|
1230
1336
|
})
|
|
1231
1337
|
.join('\n\n');
|
|
1232
1338
|
// Get base URL from servers if available
|
package/package.json
CHANGED
|
@@ -2,6 +2,43 @@ import fs from 'node:fs';
|
|
|
2
2
|
import {AbstractController} from '../controller/index.mjs';
|
|
3
3
|
import {Connector} from '../internal/index.mjs';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Build a resolvers object from a list of method names and a controller.
|
|
7
|
+
*
|
|
8
|
+
* If a method name contains dots (e.g. 'crm.contacts.create'), it is registered
|
|
9
|
+
* as a nested object tree so that resolveMethod(['crm', 'contacts', 'create'])
|
|
10
|
+
* finds the handler.
|
|
11
|
+
*
|
|
12
|
+
* If a method name has no dots (e.g. 'contactsCreate'), it is registered flat
|
|
13
|
+
* as before: resolvers.contactsCreate = handler.
|
|
14
|
+
*/
|
|
15
|
+
export function buildResolvers(methods: string[], controller: any): any {
|
|
16
|
+
const resolvers: any = {};
|
|
17
|
+
|
|
18
|
+
methods.forEach((method) => {
|
|
19
|
+
const handler = async (args: any) => {
|
|
20
|
+
if (!methods.includes(method)) throw new Error(`${method} not found`);
|
|
21
|
+
return controller[method](args);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (method.includes('.')) {
|
|
25
|
+
const parts = method.split('.');
|
|
26
|
+
let node = resolvers;
|
|
27
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
28
|
+
if (!node[parts[i]] || typeof node[parts[i]] !== 'object') {
|
|
29
|
+
node[parts[i]] = {};
|
|
30
|
+
}
|
|
31
|
+
node = node[parts[i]];
|
|
32
|
+
}
|
|
33
|
+
node[parts[parts.length - 1]] = handler;
|
|
34
|
+
} else {
|
|
35
|
+
resolvers[method] = handler;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return resolvers;
|
|
40
|
+
}
|
|
41
|
+
|
|
5
42
|
/**
|
|
6
43
|
* Runtime context to manage the lifecycle of the connector
|
|
7
44
|
*/
|
|
@@ -42,16 +79,8 @@ export default class RuntimeContext {
|
|
|
42
79
|
|
|
43
80
|
const configuration = connector.configure().config(data.config || {});
|
|
44
81
|
|
|
45
|
-
const resolvers: any = {};
|
|
46
82
|
const methods: string[] = [...data.methods, '__autocomplete', '__endpoint', '__default'];
|
|
47
|
-
|
|
48
|
-
methods.forEach((method) => {
|
|
49
|
-
resolvers[method] = async (args) => {
|
|
50
|
-
if (!methods.includes(method)) throw new Error(`${method} not found`);
|
|
51
|
-
|
|
52
|
-
return controller[method](args);
|
|
53
|
-
};
|
|
54
|
-
});
|
|
83
|
+
const resolvers = buildResolvers(methods, controller);
|
|
55
84
|
|
|
56
85
|
configuration.types(data.types).resolvers(resolvers);
|
|
57
86
|
|
package/src/cli.mts
CHANGED
|
@@ -145,6 +145,7 @@ program
|
|
|
145
145
|
)
|
|
146
146
|
.option('--multi-resource', 'Generate multiple resource files + main controller (requires multiple --spec files)')
|
|
147
147
|
.option('--controller-only', 'Generate only the controller file, do not create full project structure')
|
|
148
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
148
149
|
.option('--no-build', 'Skip installing dependencies and building the project')
|
|
149
150
|
.action(async (name, options) => {
|
|
150
151
|
name = name.replace(/[\/\.]/gi, '');
|
|
@@ -156,7 +157,8 @@ program
|
|
|
156
157
|
const spec = OpenAPIToConnector.parseSpec(specContent);
|
|
157
158
|
|
|
158
159
|
// Generate the controller from OpenAPI spec
|
|
159
|
-
const
|
|
160
|
+
const generatorOptions = {nestedPaths: !!options.nestedPaths};
|
|
161
|
+
const generator = new OpenAPIToConnector(spec, name, generatorOptions);
|
|
160
162
|
let controllerCode: string;
|
|
161
163
|
|
|
162
164
|
if (options.resource) {
|
|
@@ -278,12 +280,14 @@ program
|
|
|
278
280
|
'comma-separated list of "className:specFile" pairs (e.g., "CompaniesResource:companies.json,ContactsResource:contacts.json")'
|
|
279
281
|
)
|
|
280
282
|
.option('--base-url <url>', 'base URL for the API (if not specified, will be extracted from first OpenAPI spec)')
|
|
283
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
281
284
|
.option('--no-build', 'Skip installing dependencies and building the project')
|
|
282
285
|
.action(async (name, options) => {
|
|
283
286
|
name = name.replace(/[\/\.]/gi, '');
|
|
284
287
|
if (!name) throw new Error('name is empty');
|
|
285
288
|
|
|
286
289
|
const target = `${process.cwd()}/${name}`;
|
|
290
|
+
const generatorOptions = {nestedPaths: !!options.nestedPaths};
|
|
287
291
|
|
|
288
292
|
try {
|
|
289
293
|
// Parse resources specification
|
|
@@ -319,7 +323,7 @@ program
|
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
// Generate the resource class
|
|
322
|
-
const generator = new OpenAPIToConnector(spec, name);
|
|
326
|
+
const generator = new OpenAPIToConnector(spec, name, generatorOptions);
|
|
323
327
|
const resourceCode = generator.generateResourceClass(className);
|
|
324
328
|
|
|
325
329
|
// Write the resource file
|
|
@@ -335,7 +339,7 @@ program
|
|
|
335
339
|
// Generate the main controller
|
|
336
340
|
console.log('Generating main controller...');
|
|
337
341
|
const firstSpec = OpenAPIToConnector.parseSpec(fs.readFileSync(resourceSpecs[0].specFile, 'utf-8'));
|
|
338
|
-
const mainGenerator = new OpenAPIToConnector(firstSpec, name);
|
|
342
|
+
const mainGenerator = new OpenAPIToConnector(firstSpec, name, generatorOptions);
|
|
339
343
|
const mainControllerCode = mainGenerator.generateMainController(resources, parsedResourceSpecs);
|
|
340
344
|
|
|
341
345
|
// Write the main controller
|
|
@@ -386,6 +390,7 @@ program
|
|
|
386
390
|
.argument('<projectPath>', 'path to the existing connector project')
|
|
387
391
|
.requiredOption('--className <name>', 'class name for the resource (e.g., DealsResource)')
|
|
388
392
|
.requiredOption('--spec <file>', 'OpenAPI specification file for the new resource')
|
|
393
|
+
.option('--nested-paths', 'Use dotted namespace paths derived from URL structure (e.g., crm.contacts.getPage)')
|
|
389
394
|
.option('--no-build', 'Skip building the project after adding the resource')
|
|
390
395
|
.action(async (projectPath, options) => {
|
|
391
396
|
const target = path.resolve(projectPath);
|
|
@@ -409,7 +414,8 @@ program
|
|
|
409
414
|
const spec = OpenAPIToConnector.parseSpec(specContent);
|
|
410
415
|
|
|
411
416
|
// Generate the resource functions file (new function-based pattern)
|
|
412
|
-
const
|
|
417
|
+
const generatorOptions = {nestedPaths: !!options.nestedPaths};
|
|
418
|
+
const generator = new OpenAPIToConnector(spec, 'Resource', generatorOptions);
|
|
413
419
|
const resourceCode = generator.generateResourceClass(options.className);
|
|
414
420
|
|
|
415
421
|
// Write the resource file
|
|
@@ -476,7 +482,7 @@ program
|
|
|
476
482
|
const resourceSpecs = [{fileName, spec}];
|
|
477
483
|
|
|
478
484
|
// Create a temporary generator to generate just the exposed methods for this resource
|
|
479
|
-
const tempGenerator = new OpenAPIToConnector(spec, 'temp');
|
|
485
|
+
const tempGenerator = new OpenAPIToConnector(spec, 'temp', generatorOptions);
|
|
480
486
|
const exposedMethods = tempGenerator.generateExposedResourceMethods(resources, resourceSpecs);
|
|
481
487
|
|
|
482
488
|
// Add the exposed methods to the controller before the closing brace
|