@aloma.io/integration-sdk 3.8.50 → 3.8.51
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/OPENAPI_TO_CONNECTOR.md +216 -0
- package/README.md +15 -1
- package/build/cli.mjs +57 -1
- package/build/internal/fetcher/fetcher.d.mts +1 -0
- package/build/openapi-to-connector.d.mts +35 -0
- package/build/openapi-to-connector.mjs +207 -0
- package/examples/generate-connector.sh +35 -0
- package/examples/generated-controller.mts +81 -0
- package/examples/sample-api.json +325 -0
- package/examples/sample-api.yaml +222 -0
- package/examples/simple-api.json +39 -0
- package/package.json +11 -4
- package/src/builder/index.mts +3 -3
- package/src/builder/runtime-context.mts +2 -2
- package/src/cli.mts +65 -1
- package/src/controller/index.mts +2 -2
- package/src/internal/connector/server/on-connect/index.mts +9 -9
- package/src/internal/fetcher/fetcher.mts +5 -5
- package/src/internal/websocket/transport/index.mjs +4 -5
- package/src/openapi-to-connector.mts +255 -0
- package/test/openapi-to-connector.test.mts +207 -0
package/src/cli.mts
CHANGED
@@ -10,6 +10,7 @@ import {TARGET_DIR} from './builder/index.mjs';
|
|
10
10
|
import {notEmpty} from './internal/util/index.mjs';
|
11
11
|
import JWE from './internal/util/jwe/index.mjs';
|
12
12
|
import parseTypes from './transform/index.mjs';
|
13
|
+
import {OpenAPIToConnector} from './openapi-to-connector.mjs';
|
13
14
|
|
14
15
|
const exec = util.promisify(ChildProcess.exec);
|
15
16
|
|
@@ -101,7 +102,7 @@ program
|
|
101
102
|
await generateKeys({target});
|
102
103
|
|
103
104
|
console.log('Installing dependencies ...');
|
104
|
-
await exec(`cd ${target}; yarn`);
|
105
|
+
await exec(`cd ${target}; yarn --ignore-engines`);
|
105
106
|
|
106
107
|
console.log('Building ...');
|
107
108
|
await exec(`cd ${target}; yarn build`);
|
@@ -131,6 +132,69 @@ program
|
|
131
132
|
new Extractor().extract('./src/controller/index.mts', './build/.controller.json');
|
132
133
|
});
|
133
134
|
|
135
|
+
program
|
136
|
+
.command('from-openapi')
|
137
|
+
.description('Generate a connector controller from an OpenAPI specification')
|
138
|
+
.argument('<name>', 'name of the connector project')
|
139
|
+
.requiredOption('--connector-id <id>', 'id of the connector')
|
140
|
+
.requiredOption('--spec <file>', 'OpenAPI specification file (JSON or YAML)')
|
141
|
+
.option('--out <file>', 'output file path for the controller', 'src/controller/index.mts')
|
142
|
+
.action(async (name, options) => {
|
143
|
+
name = name.replace(/[\/\.]/gi, '');
|
144
|
+
if (!name) throw new Error('name is empty');
|
145
|
+
|
146
|
+
const target = `${process.cwd()}/${name}`;
|
147
|
+
|
148
|
+
try {
|
149
|
+
// Read and parse the OpenAPI spec
|
150
|
+
const specContent = fs.readFileSync(options.spec, 'utf-8');
|
151
|
+
const spec = OpenAPIToConnector.parseSpec(specContent);
|
152
|
+
|
153
|
+
// Create the connector project structure
|
154
|
+
fs.mkdirSync(target);
|
155
|
+
console.log('Creating connector project...');
|
156
|
+
extract({...options, target, name});
|
157
|
+
|
158
|
+
// Generate the controller from OpenAPI spec
|
159
|
+
console.log('Generating controller from OpenAPI specification...');
|
160
|
+
const generator = new OpenAPIToConnector(spec, name);
|
161
|
+
const controllerCode = generator.generateController();
|
162
|
+
|
163
|
+
// Write the generated controller
|
164
|
+
const controllerPath = `${target}/${options.out}`;
|
165
|
+
fs.mkdirSync(path.dirname(controllerPath), {recursive: true});
|
166
|
+
fs.writeFileSync(controllerPath, controllerCode);
|
167
|
+
|
168
|
+
console.log('Generating keys...');
|
169
|
+
await generateKeys({target});
|
170
|
+
|
171
|
+
console.log('Installing dependencies...');
|
172
|
+
await exec(`cd ${target}; yarn --ignore-engines`);
|
173
|
+
|
174
|
+
console.log('Building...');
|
175
|
+
await exec(`cd ${target}; yarn build`);
|
176
|
+
|
177
|
+
console.log(`
|
178
|
+
✅ Success! Generated connector from OpenAPI specification
|
179
|
+
📝 Connector name: ${name}
|
180
|
+
📊 Found ${generator.getOperationsCount()} operations
|
181
|
+
📄 Controller generated: ${options.out}
|
182
|
+
|
183
|
+
Next steps:
|
184
|
+
1.) Add the connector to a workspace
|
185
|
+
2.) Edit ./${name}/.env and insert the registration token
|
186
|
+
3.) Implement the actual API calls in each method in ${options.out}
|
187
|
+
4.) Start the connector with cd ./${name}/; yarn start`);
|
188
|
+
} catch (error) {
|
189
|
+
console.error('❌ Error:', error instanceof Error ? error.message : 'Unknown error');
|
190
|
+
// Clean up on error
|
191
|
+
if (fs.existsSync(target)) {
|
192
|
+
fs.rmSync(target, {recursive: true, force: true});
|
193
|
+
}
|
194
|
+
process.exit(1);
|
195
|
+
}
|
196
|
+
});
|
197
|
+
|
134
198
|
class Extractor {
|
135
199
|
async extract(source, target) {
|
136
200
|
notEmpty(source, 'source');
|
package/src/controller/index.mts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
import {
|
1
|
+
import {ConfigField} from '../index.mjs';
|
2
2
|
import Fetcher from '../internal/fetcher/fetcher.mjs';
|
3
|
-
import {
|
3
|
+
import {OAuth} from '../internal/fetcher/oauth-fetcher.mjs';
|
4
4
|
|
5
5
|
/**
|
6
6
|
* Abstract controller class
|
@@ -1,12 +1,12 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
1
|
+
import {init} from '@paralleldrive/cuid2';
|
2
|
+
import {AbstractController} from '../../../../index.mjs';
|
3
3
|
import Dispatcher from '../../../dispatcher/index.mjs';
|
4
4
|
import Fetcher from '../../../fetcher/fetcher.mjs';
|
5
|
-
import {
|
6
|
-
import {
|
7
|
-
import {
|
8
|
-
import {
|
9
|
-
import {
|
5
|
+
import {Config} from '../../../websocket/config.mjs';
|
6
|
+
import {decryptConfig} from './decrypt-config.mjs';
|
7
|
+
import {patchFinishOAuth} from './finish-oauth.mjs';
|
8
|
+
import {makeOAuth} from './make-oauth.mjs';
|
9
|
+
import {patchStartOAuth} from './start-oauth.mjs';
|
10
10
|
|
11
11
|
const cuid = init({length: 32});
|
12
12
|
|
@@ -28,9 +28,9 @@ export const onConnect = ({
|
|
28
28
|
secrets,
|
29
29
|
config,
|
30
30
|
});
|
31
|
-
|
31
|
+
|
32
32
|
if (decrypted?.oauthResult?.scope) {
|
33
|
-
console.log(`Scope: ${decrypted.oauthResult.scope}`)
|
33
|
+
console.log(`Scope: ${decrypted.oauthResult.scope}`);
|
34
34
|
}
|
35
35
|
|
36
36
|
await patchStartOAuth({dispatcher, decrypted});
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import {unwrap} from '../util/index.mjs';
|
2
2
|
|
3
3
|
/**
|
4
4
|
* http request fetcher
|
@@ -17,7 +17,7 @@ export default class Fetcher {
|
|
17
17
|
retry?: number;
|
18
18
|
baseUrl?: string;
|
19
19
|
onResponse?: (response: Response) => void;
|
20
|
-
customize?: (request: {
|
20
|
+
customize?: (request: {[key: string]: any}) => void;
|
21
21
|
} = {}) {
|
22
22
|
this.retry = retry;
|
23
23
|
this.baseUrl = baseUrl;
|
@@ -68,7 +68,7 @@ export default class Fetcher {
|
|
68
68
|
/**
|
69
69
|
* request headers like Accept, Content-type
|
70
70
|
*/
|
71
|
-
headers?: {
|
71
|
+
headers?: {[key: string]: any};
|
72
72
|
/**
|
73
73
|
* request body like "hello world" or {hello: "world"}
|
74
74
|
*/
|
@@ -81,7 +81,7 @@ export default class Fetcher {
|
|
81
81
|
baseUrl = local.baseUrl;
|
82
82
|
options ||= {};
|
83
83
|
|
84
|
-
const options0: any = {
|
84
|
+
const options0: any = {...options};
|
85
85
|
|
86
86
|
if (retries == null) retries = local.retry;
|
87
87
|
|
@@ -139,7 +139,7 @@ export default class Fetcher {
|
|
139
139
|
}
|
140
140
|
|
141
141
|
if (status === 204) {
|
142
|
-
return {
|
142
|
+
return {ok: true};
|
143
143
|
}
|
144
144
|
|
145
145
|
return unwrap(ret, options0);
|
@@ -1,10 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import {init} from '@paralleldrive/cuid2';
|
2
2
|
import C from '../connection/constants.mjs';
|
3
3
|
const cuid = init({length: 32});
|
4
4
|
|
5
5
|
import WebSocket from 'ws';
|
6
|
-
import {
|
7
|
-
import {
|
6
|
+
import {DurableWebsocket} from './durable.mjs';
|
7
|
+
import {Callback, Packet} from './packet.mjs';
|
8
8
|
|
9
9
|
const cleanInterval = 45 * 1000;
|
10
10
|
const pingInterval = 30 * 1000;
|
@@ -191,5 +191,4 @@ class Transport {
|
|
191
191
|
}
|
192
192
|
}
|
193
193
|
|
194
|
-
export {
|
195
|
-
|
194
|
+
export {Transport};
|
@@ -0,0 +1,255 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import {Command} from 'commander';
|
4
|
+
import fs from 'node:fs';
|
5
|
+
import path from 'node:path';
|
6
|
+
import yaml from 'js-yaml';
|
7
|
+
import {OpenAPIV3} from 'openapi-types';
|
8
|
+
import {z} from 'zod';
|
9
|
+
|
10
|
+
// OpenAPI 3.x validation schema
|
11
|
+
const OpenAPISchema = z.object({
|
12
|
+
openapi: z.string().regex(/^3\.\d+\.\d+$/),
|
13
|
+
info: z.object({
|
14
|
+
title: z.string(),
|
15
|
+
version: z.string(),
|
16
|
+
description: z.string().optional(),
|
17
|
+
}),
|
18
|
+
paths: z.record(z.string(), z.any()),
|
19
|
+
servers: z
|
20
|
+
.array(
|
21
|
+
z.object({
|
22
|
+
url: z.string(),
|
23
|
+
description: z.string().optional(),
|
24
|
+
})
|
25
|
+
)
|
26
|
+
.optional(),
|
27
|
+
components: z.any().optional(),
|
28
|
+
});
|
29
|
+
|
30
|
+
interface OperationInfo {
|
31
|
+
method: string;
|
32
|
+
path: string;
|
33
|
+
operationId?: string;
|
34
|
+
summary?: string;
|
35
|
+
description?: string;
|
36
|
+
parameters?: any[];
|
37
|
+
requestBody?: any;
|
38
|
+
responses?: any;
|
39
|
+
}
|
40
|
+
|
41
|
+
export class OpenAPIToConnector {
|
42
|
+
private spec: OpenAPIV3.Document;
|
43
|
+
private connectorName: string;
|
44
|
+
|
45
|
+
constructor(spec: OpenAPIV3.Document, connectorName: string) {
|
46
|
+
this.spec = spec;
|
47
|
+
this.connectorName = connectorName;
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Parse OpenAPI spec from JSON or YAML string
|
52
|
+
*/
|
53
|
+
static parseSpec(specString: string): OpenAPIV3.Document {
|
54
|
+
let parsed: any;
|
55
|
+
|
56
|
+
try {
|
57
|
+
// Try parsing as JSON first
|
58
|
+
parsed = JSON.parse(specString);
|
59
|
+
} catch {
|
60
|
+
try {
|
61
|
+
// If JSON fails, try YAML
|
62
|
+
parsed = yaml.load(specString);
|
63
|
+
} catch (error) {
|
64
|
+
throw new Error(`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
// Validate against OpenAPI 3.x schema
|
69
|
+
const validationResult = OpenAPISchema.safeParse(parsed);
|
70
|
+
if (!validationResult.success) {
|
71
|
+
const errors = validationResult.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ');
|
72
|
+
throw new Error(`Invalid OpenAPI 3.x specification: ${errors}`);
|
73
|
+
}
|
74
|
+
|
75
|
+
return parsed as OpenAPIV3.Document;
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Extract all operations from the OpenAPI spec
|
80
|
+
*/
|
81
|
+
private extractOperations(): OperationInfo[] {
|
82
|
+
const operations: OperationInfo[] = [];
|
83
|
+
|
84
|
+
for (const [path, pathItem] of Object.entries(this.spec.paths)) {
|
85
|
+
if (!pathItem) continue;
|
86
|
+
|
87
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] as const;
|
88
|
+
|
89
|
+
for (const method of methods) {
|
90
|
+
const operation = pathItem[method];
|
91
|
+
if (!operation) continue;
|
92
|
+
|
93
|
+
operations.push({
|
94
|
+
method: method.toUpperCase(),
|
95
|
+
path,
|
96
|
+
operationId: operation.operationId,
|
97
|
+
summary: operation.summary,
|
98
|
+
description: operation.description,
|
99
|
+
parameters: operation.parameters,
|
100
|
+
requestBody: operation.requestBody,
|
101
|
+
responses: operation.responses,
|
102
|
+
});
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
return operations;
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Generate a valid JavaScript identifier from a string
|
111
|
+
*/
|
112
|
+
private toValidIdentifier(str: string): string {
|
113
|
+
return str
|
114
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
115
|
+
.replace(/^[0-9]/, '_$&')
|
116
|
+
.replace(/_+/g, '_')
|
117
|
+
.replace(/^_|_$/g, '');
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Generate method name from operation info
|
122
|
+
*/
|
123
|
+
private generateMethodName(operation: OperationInfo): string {
|
124
|
+
if (operation.operationId) {
|
125
|
+
return this.toValidIdentifier(operation.operationId);
|
126
|
+
}
|
127
|
+
|
128
|
+
// Generate from method + path
|
129
|
+
const pathParts = operation.path
|
130
|
+
.replace(/[{}]/g, '') // Remove path parameters
|
131
|
+
.split('/')
|
132
|
+
.filter((part) => part.length > 0)
|
133
|
+
.map((part) => this.toValidIdentifier(part));
|
134
|
+
|
135
|
+
const methodPrefix = operation.method.toLowerCase();
|
136
|
+
const pathSuffix = pathParts.join('_') || 'root';
|
137
|
+
|
138
|
+
return `${methodPrefix}_${pathSuffix}`;
|
139
|
+
}
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Generate JSDoc comment for an operation
|
143
|
+
*/
|
144
|
+
private generateJSDoc(operation: OperationInfo): string {
|
145
|
+
const lines: string[] = [];
|
146
|
+
|
147
|
+
if (operation.summary) {
|
148
|
+
lines.push(` * ${operation.summary}`);
|
149
|
+
}
|
150
|
+
|
151
|
+
if (operation.description) {
|
152
|
+
lines.push(` * ${operation.description}`);
|
153
|
+
}
|
154
|
+
|
155
|
+
if (operation.parameters && operation.parameters.length > 0) {
|
156
|
+
lines.push(' *');
|
157
|
+
lines.push(' * @param args - Request arguments');
|
158
|
+
for (const param of operation.parameters) {
|
159
|
+
if (typeof param === 'object' && 'name' in param && 'description' in param) {
|
160
|
+
lines.push(` * @param args.${param.name} - ${param.description || 'Parameter'}`);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
if (operation.requestBody) {
|
166
|
+
lines.push(' *');
|
167
|
+
lines.push(' * @param args.body - Request body');
|
168
|
+
}
|
169
|
+
|
170
|
+
lines.push(' * @returns Response data');
|
171
|
+
|
172
|
+
return lines.join('\n');
|
173
|
+
}
|
174
|
+
|
175
|
+
/**
|
176
|
+
* Get the number of operations in the OpenAPI spec
|
177
|
+
*/
|
178
|
+
getOperationsCount(): number {
|
179
|
+
return this.extractOperations().length;
|
180
|
+
}
|
181
|
+
|
182
|
+
/**
|
183
|
+
* Generate the connector controller code
|
184
|
+
*/
|
185
|
+
generateController(): string {
|
186
|
+
const operations = this.extractOperations();
|
187
|
+
|
188
|
+
if (operations.length === 0) {
|
189
|
+
throw new Error('No operations found in OpenAPI specification');
|
190
|
+
}
|
191
|
+
|
192
|
+
const methods = operations
|
193
|
+
.map((operation) => {
|
194
|
+
const methodName = this.generateMethodName(operation);
|
195
|
+
const jsdoc = this.generateJSDoc(operation);
|
196
|
+
|
197
|
+
return ` /**\n${jsdoc}\n */\n async ${methodName}(args: any) {\n // TODO: Implement ${operation.method} ${operation.path}\n throw new Error('Method not implemented');\n }`;
|
198
|
+
})
|
199
|
+
.join('\n\n');
|
200
|
+
|
201
|
+
return `import {AbstractController} from '@aloma.io/integration-sdk';
|
202
|
+
|
203
|
+
export default class Controller extends AbstractController {
|
204
|
+
|
205
|
+
${methods}
|
206
|
+
}`;
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
// CLI setup
|
211
|
+
const program = new Command();
|
212
|
+
|
213
|
+
program
|
214
|
+
.name('openapi-to-connector')
|
215
|
+
.description('Generate a connector controller from an OpenAPI specification')
|
216
|
+
.version('1.0.0')
|
217
|
+
.showHelpAfterError();
|
218
|
+
|
219
|
+
program
|
220
|
+
.command('generate')
|
221
|
+
.description('Generate connector controller from OpenAPI spec')
|
222
|
+
.requiredOption('--name <name>', 'Human-readable connector name')
|
223
|
+
.requiredOption('--spec <file>', 'OpenAPI specification file (JSON or YAML)')
|
224
|
+
.option('--out <file>', 'Output file path', 'index.mts')
|
225
|
+
.action(async (options) => {
|
226
|
+
try {
|
227
|
+
// Read and parse the OpenAPI spec
|
228
|
+
const specContent = fs.readFileSync(options.spec, 'utf-8');
|
229
|
+
const spec = OpenAPIToConnector.parseSpec(specContent);
|
230
|
+
|
231
|
+
// Generate the connector controller
|
232
|
+
const generator = new OpenAPIToConnector(spec, options.name);
|
233
|
+
const controllerCode = generator.generateController();
|
234
|
+
|
235
|
+
// Write the output file
|
236
|
+
fs.writeFileSync(options.out, controllerCode);
|
237
|
+
|
238
|
+
console.log(`✅ Successfully generated connector controller: ${options.out}`);
|
239
|
+
console.log(`📝 Connector name: ${options.name}`);
|
240
|
+
console.log(`📊 Found ${generator['extractOperations']().length} operations`);
|
241
|
+
} catch (error) {
|
242
|
+
console.error('❌ Error:', error instanceof Error ? error.message : 'Unknown error');
|
243
|
+
process.exit(1);
|
244
|
+
}
|
245
|
+
});
|
246
|
+
|
247
|
+
// Only run CLI if this file is executed directly
|
248
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
249
|
+
// If no command is provided, show help
|
250
|
+
if (process.argv.length <= 2) {
|
251
|
+
program.help();
|
252
|
+
} else {
|
253
|
+
program.parse();
|
254
|
+
}
|
255
|
+
}
|
@@ -0,0 +1,207 @@
|
|
1
|
+
import { describe, it } from 'mocha';
|
2
|
+
import { expect } from 'chai';
|
3
|
+
import { OpenAPIToConnector } from '../build/openapi-to-connector.mjs';
|
4
|
+
|
5
|
+
describe('OpenAPIToConnector', () => {
|
6
|
+
const validOpenAPISpec = {
|
7
|
+
openapi: '3.0.0' as const,
|
8
|
+
info: {
|
9
|
+
title: 'Test API',
|
10
|
+
version: '1.0.0',
|
11
|
+
description: 'A test API'
|
12
|
+
},
|
13
|
+
paths: {
|
14
|
+
'/users': {
|
15
|
+
get: {
|
16
|
+
operationId: 'getUsers',
|
17
|
+
summary: 'Get all users',
|
18
|
+
description: 'Retrieve a list of all users',
|
19
|
+
responses: {
|
20
|
+
'200': {
|
21
|
+
description: 'Successful response',
|
22
|
+
content: {
|
23
|
+
'application/json': {
|
24
|
+
schema: {
|
25
|
+
type: 'array' as const,
|
26
|
+
items: {
|
27
|
+
type: 'object' as const,
|
28
|
+
properties: {
|
29
|
+
id: { type: 'string' as const },
|
30
|
+
name: { type: 'string' as const }
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
},
|
39
|
+
post: {
|
40
|
+
operationId: 'createUser',
|
41
|
+
summary: 'Create a new user',
|
42
|
+
description: 'Create a new user in the system',
|
43
|
+
requestBody: {
|
44
|
+
required: true,
|
45
|
+
content: {
|
46
|
+
'application/json': {
|
47
|
+
schema: {
|
48
|
+
type: 'object' as const,
|
49
|
+
properties: {
|
50
|
+
name: { type: 'string' as const },
|
51
|
+
email: { type: 'string' as const }
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
},
|
57
|
+
responses: {
|
58
|
+
'201': {
|
59
|
+
description: 'User created successfully'
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
},
|
64
|
+
'/users/{id}': {
|
65
|
+
get: {
|
66
|
+
operationId: 'getUserById',
|
67
|
+
summary: 'Get user by ID',
|
68
|
+
parameters: [
|
69
|
+
{
|
70
|
+
name: 'id',
|
71
|
+
in: 'path' as const,
|
72
|
+
required: true,
|
73
|
+
schema: { type: 'string' as const },
|
74
|
+
description: 'User ID'
|
75
|
+
}
|
76
|
+
],
|
77
|
+
responses: {
|
78
|
+
'200': {
|
79
|
+
description: 'User found'
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
};
|
86
|
+
|
87
|
+
describe('parseSpec', () => {
|
88
|
+
it('should parse valid OpenAPI 3.0 JSON spec', () => {
|
89
|
+
const specString = JSON.stringify(validOpenAPISpec);
|
90
|
+
const result = OpenAPIToConnector.parseSpec(specString);
|
91
|
+
|
92
|
+
expect(result.openapi).to.equal('3.0.0');
|
93
|
+
expect(result.info.title).to.equal('Test API');
|
94
|
+
expect(result.paths).to.have.property('/users');
|
95
|
+
});
|
96
|
+
|
97
|
+
it('should parse valid OpenAPI 3.0 YAML spec', () => {
|
98
|
+
const yamlSpec = `
|
99
|
+
openapi: 3.0.0
|
100
|
+
info:
|
101
|
+
title: Test API
|
102
|
+
version: 1.0.0
|
103
|
+
description: A test API
|
104
|
+
paths:
|
105
|
+
/users:
|
106
|
+
get:
|
107
|
+
operationId: getUsers
|
108
|
+
summary: Get all users
|
109
|
+
responses:
|
110
|
+
'200':
|
111
|
+
description: Successful response
|
112
|
+
`;
|
113
|
+
const result = OpenAPIToConnector.parseSpec(yamlSpec);
|
114
|
+
|
115
|
+
expect(result.openapi).to.equal('3.0.0');
|
116
|
+
expect(result.info.title).to.equal('Test API');
|
117
|
+
});
|
118
|
+
|
119
|
+
it('should reject invalid OpenAPI version', () => {
|
120
|
+
const invalidSpec = {
|
121
|
+
...validOpenAPISpec,
|
122
|
+
openapi: '2.0.0'
|
123
|
+
};
|
124
|
+
|
125
|
+
expect(() => {
|
126
|
+
OpenAPIToConnector.parseSpec(JSON.stringify(invalidSpec));
|
127
|
+
}).to.throw('Invalid OpenAPI 3.x specification');
|
128
|
+
});
|
129
|
+
|
130
|
+
it('should reject spec without required fields', () => {
|
131
|
+
const invalidSpec = {
|
132
|
+
openapi: '3.0.0'
|
133
|
+
// Missing info and paths
|
134
|
+
};
|
135
|
+
|
136
|
+
expect(() => {
|
137
|
+
OpenAPIToConnector.parseSpec(JSON.stringify(invalidSpec));
|
138
|
+
}).to.throw('Invalid OpenAPI 3.x specification');
|
139
|
+
});
|
140
|
+
|
141
|
+
it('should reject invalid JSON/YAML', () => {
|
142
|
+
expect(() => {
|
143
|
+
OpenAPIToConnector.parseSpec('invalid json {');
|
144
|
+
}).to.throw('Failed to parse OpenAPI spec');
|
145
|
+
});
|
146
|
+
});
|
147
|
+
|
148
|
+
describe('generateController', () => {
|
149
|
+
it('should generate controller with all operations', () => {
|
150
|
+
const generator = new OpenAPIToConnector(validOpenAPISpec, 'Test Connector');
|
151
|
+
const controllerCode = generator.generateController();
|
152
|
+
|
153
|
+
expect(controllerCode).to.include('import {AbstractController}');
|
154
|
+
expect(controllerCode).to.include('export default class Controller extends AbstractController');
|
155
|
+
expect(controllerCode).to.include('async getUsers(args: any)');
|
156
|
+
expect(controllerCode).to.include('async createUser(args: any)');
|
157
|
+
expect(controllerCode).to.include('async get_users_{id}(args: any)');
|
158
|
+
});
|
159
|
+
|
160
|
+
it('should generate proper JSDoc comments', () => {
|
161
|
+
const generator = new OpenAPIToConnector(validOpenAPISpec, 'Test Connector');
|
162
|
+
const controllerCode = generator.generateController();
|
163
|
+
|
164
|
+
expect(controllerCode).to.include('* Get all users');
|
165
|
+
expect(controllerCode).to.include('* Retrieve a list of all users');
|
166
|
+
expect(controllerCode).to.include('* @param args.body - Request body');
|
167
|
+
expect(controllerCode).to.include('* @param args.id - User ID');
|
168
|
+
});
|
169
|
+
|
170
|
+
it('should handle operations without operationId', () => {
|
171
|
+
const specWithoutOperationId = {
|
172
|
+
...validOpenAPISpec,
|
173
|
+
paths: {
|
174
|
+
'/test': {
|
175
|
+
get: {
|
176
|
+
summary: 'Test endpoint',
|
177
|
+
responses: {
|
178
|
+
'200': {
|
179
|
+
description: 'Success'
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}
|
185
|
+
};
|
186
|
+
|
187
|
+
const generator = new OpenAPIToConnector(specWithoutOperationId, 'Test Connector');
|
188
|
+
const controllerCode = generator.generateController();
|
189
|
+
|
190
|
+
expect(controllerCode).to.include('async get_test(args: any)');
|
191
|
+
});
|
192
|
+
|
193
|
+
it('should throw error for empty spec', () => {
|
194
|
+
const emptySpec = {
|
195
|
+
openapi: '3.0.0',
|
196
|
+
info: { title: 'Test', version: '1.0.0' },
|
197
|
+
paths: {}
|
198
|
+
};
|
199
|
+
|
200
|
+
const generator = new OpenAPIToConnector(emptySpec, 'Test Connector');
|
201
|
+
|
202
|
+
expect(() => {
|
203
|
+
generator.generateController();
|
204
|
+
}).to.throw('No operations found in OpenAPI specification');
|
205
|
+
});
|
206
|
+
});
|
207
|
+
});
|