@aloma.io/integration-sdk 3.8.50 → 3.8.52

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.
@@ -1,8 +1,8 @@
1
1
  import 'dotenv/config';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { notEmpty } from '../internal/util/index.mjs';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {notEmpty} from '../internal/util/index.mjs';
6
6
  import RuntimeContext from './runtime-context.mjs';
7
7
 
8
8
  const DIR_OFFSET = '/../../../../../';
@@ -33,7 +33,7 @@ export type ConfigField =
33
33
  /**
34
34
  * minimum height of the field
35
35
  */
36
- height?: number
36
+ height?: number;
37
37
  /**
38
38
  * the type of the field
39
39
  */
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
- import { AbstractController } from '../controller/index.mjs';
3
- import { Connector } from '../internal/index.mjs';
2
+ import {AbstractController} from '../controller/index.mjs';
3
+ import {Connector} from '../internal/index.mjs';
4
4
 
5
5
  /**
6
6
  * Runtime context to manage the lifecycle of the connector
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,305 @@
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
+ // Clean up HubSpot-style operationIds like "get-/crm/v3/objects/companies_getPage"
126
+ let cleaned = operation.operationId;
127
+
128
+ // Extract the last part after underscore if it exists
129
+ const parts = cleaned.split('_');
130
+ if (parts.length > 1) {
131
+ const lastPart = parts[parts.length - 1];
132
+ // If the last part looks like a method name (camelCase), use it
133
+ if (lastPart && /^[a-z][a-zA-Z0-9]*$/.test(lastPart)) {
134
+ cleaned = lastPart;
135
+ }
136
+ }
137
+
138
+ // Remove any remaining special characters and clean up
139
+ cleaned = cleaned
140
+ .replace(/^(get|post|put|patch|delete|head|options)-/i, '') // Remove HTTP method prefix
141
+ .replace(/[^a-zA-Z0-9_]/g, '_')
142
+ .replace(/_+/g, '_')
143
+ .replace(/^_|_$/g, '');
144
+
145
+ // If we still have a valid identifier, use it
146
+ if (cleaned && /^[a-zA-Z]/.test(cleaned)) {
147
+ return cleaned;
148
+ }
149
+ }
150
+
151
+ // Generate from method + path
152
+ const pathParts = operation.path
153
+ .replace(/[{}]/g, '') // Remove path parameters
154
+ .split('/')
155
+ .filter((part) => part.length > 0)
156
+ .map((part) => this.toValidIdentifier(part));
157
+
158
+ const methodPrefix = operation.method.toLowerCase();
159
+ const pathSuffix = pathParts.join('_') || 'root';
160
+
161
+ return `${methodPrefix}_${pathSuffix}`;
162
+ }
163
+
164
+ /**
165
+ * Generate JSDoc comment for an operation
166
+ */
167
+ private generateJSDoc(operation: OperationInfo): string {
168
+ const lines: string[] = [];
169
+
170
+ if (operation.summary) {
171
+ lines.push(` * ${operation.summary}`);
172
+ }
173
+
174
+ if (operation.description) {
175
+ lines.push(` *`);
176
+ // Split long descriptions into multiple lines
177
+ const descLines = operation.description.split('\n');
178
+ descLines.forEach(line => {
179
+ if (line.trim()) {
180
+ lines.push(` * ${line.trim()}`);
181
+ }
182
+ });
183
+ }
184
+
185
+ // Document parameters with full details
186
+ if (operation.parameters && operation.parameters.length > 0) {
187
+ lines.push(' *');
188
+ lines.push(' * @param {Object} args - Request arguments');
189
+
190
+ for (const param of operation.parameters) {
191
+ if (typeof param === 'object' && 'name' in param) {
192
+ const paramName = param.name;
193
+ const paramDesc = param.description || '';
194
+ const paramRequired = param.required ? '(required)' : '(optional)';
195
+ const paramType = param.schema?.type || 'any';
196
+ const paramIn = param.in || '';
197
+
198
+ let paramDoc = ` * @param {${paramType}} args.${paramName} ${paramRequired}`;
199
+ if (paramDesc) {
200
+ paramDoc += ` - ${paramDesc}`;
201
+ }
202
+ if (paramIn) {
203
+ paramDoc += ` [${paramIn}]`;
204
+ }
205
+ lines.push(paramDoc);
206
+ }
207
+ }
208
+ }
209
+
210
+ // Document request body
211
+ if (operation.requestBody) {
212
+ lines.push(' *');
213
+ const bodyDesc = operation.requestBody.description || 'Request body';
214
+ const required = operation.requestBody.required ? '(required)' : '(optional)';
215
+ lines.push(` * @param {Object} args.body ${required} - ${bodyDesc}`);
216
+ }
217
+
218
+ // Document response
219
+ lines.push(' *');
220
+ lines.push(` * @returns {Promise<Object>} ${operation.method} ${operation.path} response`);
221
+
222
+ return lines.join('\n');
223
+ }
224
+
225
+ /**
226
+ * Get the number of operations in the OpenAPI spec
227
+ */
228
+ getOperationsCount(): number {
229
+ return this.extractOperations().length;
230
+ }
231
+
232
+ /**
233
+ * Generate the connector controller code
234
+ */
235
+ generateController(): string {
236
+ const operations = this.extractOperations();
237
+
238
+ if (operations.length === 0) {
239
+ throw new Error('No operations found in OpenAPI specification');
240
+ }
241
+
242
+ const methods = operations
243
+ .map((operation) => {
244
+ const methodName = this.generateMethodName(operation);
245
+ const jsdoc = this.generateJSDoc(operation);
246
+
247
+ return ` /**\n${jsdoc}\n */\n async ${methodName}(args: any) {\n // TODO: Implement ${operation.method} ${operation.path}\n throw new Error('Method not implemented');\n }`;
248
+ })
249
+ .join('\n\n');
250
+
251
+ return `import {AbstractController} from '@aloma.io/integration-sdk';
252
+
253
+ export default class Controller extends AbstractController {
254
+
255
+ ${methods}
256
+ }`;
257
+ }
258
+ }
259
+
260
+ // CLI setup
261
+ const program = new Command();
262
+
263
+ program
264
+ .name('openapi-to-connector')
265
+ .description('Generate a connector controller from an OpenAPI specification')
266
+ .version('1.0.0')
267
+ .showHelpAfterError();
268
+
269
+ program
270
+ .command('generate')
271
+ .description('Generate connector controller from OpenAPI spec')
272
+ .requiredOption('--name <name>', 'Human-readable connector name')
273
+ .requiredOption('--spec <file>', 'OpenAPI specification file (JSON or YAML)')
274
+ .option('--out <file>', 'Output file path', 'index.mts')
275
+ .action(async (options) => {
276
+ try {
277
+ // Read and parse the OpenAPI spec
278
+ const specContent = fs.readFileSync(options.spec, 'utf-8');
279
+ const spec = OpenAPIToConnector.parseSpec(specContent);
280
+
281
+ // Generate the connector controller
282
+ const generator = new OpenAPIToConnector(spec, options.name);
283
+ const controllerCode = generator.generateController();
284
+
285
+ // Write the output file
286
+ fs.writeFileSync(options.out, controllerCode);
287
+
288
+ console.log(`✅ Successfully generated connector controller: ${options.out}`);
289
+ console.log(`📝 Connector name: ${options.name}`);
290
+ console.log(`📊 Found ${generator['extractOperations']().length} operations`);
291
+ } catch (error) {
292
+ console.error('❌ Error:', error instanceof Error ? error.message : 'Unknown error');
293
+ process.exit(1);
294
+ }
295
+ });
296
+
297
+ // Only run CLI if this file is executed directly
298
+ if (import.meta.url === `file://${process.argv[1]}`) {
299
+ // If no command is provided, show help
300
+ if (process.argv.length <= 2) {
301
+ program.help();
302
+ } else {
303
+ program.parse();
304
+ }
305
+ }