@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/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');
@@ -1,6 +1,6 @@
1
- import { ConfigField } from '../index.mjs';
1
+ import {ConfigField} from '../index.mjs';
2
2
  import Fetcher from '../internal/fetcher/fetcher.mjs';
3
- import { OAuth } from '../internal/fetcher/oauth-fetcher.mjs';
3
+ import {OAuth} from '../internal/fetcher/oauth-fetcher.mjs';
4
4
 
5
5
  /**
6
6
  * Abstract controller class
@@ -1,12 +1,12 @@
1
- import { init } from '@paralleldrive/cuid2';
2
- import { AbstractController } from '../../../../index.mjs';
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 { 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';
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 { unwrap } from '../util/index.mjs';
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: { [key: string]: any }) => void;
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?: { [key: string]: any };
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 = { ...options };
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 { ok: true };
142
+ return {ok: true};
143
143
  }
144
144
 
145
145
  return unwrap(ret, options0);
@@ -1,10 +1,10 @@
1
- import { init } from '@paralleldrive/cuid2';
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 { DurableWebsocket } from './durable.mjs';
7
- import { Callback, Packet } from './packet.mjs';
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 { Transport };
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
+ });