@aifabrix/builder 2.8.0 → 2.9.0

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.
Files changed (36) hide show
  1. package/integration/hubspot/README.md +136 -0
  2. package/integration/hubspot/env.template +9 -0
  3. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  4. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  5. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  6. package/integration/hubspot/hubspot-deploy.json +91 -0
  7. package/integration/hubspot/variables.yaml +17 -0
  8. package/lib/app-config.js +4 -3
  9. package/lib/app-deploy.js +8 -20
  10. package/lib/app-dockerfile.js +7 -9
  11. package/lib/app-prompts.js +6 -5
  12. package/lib/app-push.js +9 -9
  13. package/lib/app-register.js +23 -5
  14. package/lib/app-rotate-secret.js +10 -0
  15. package/lib/app-run.js +5 -11
  16. package/lib/app.js +42 -14
  17. package/lib/build.js +20 -16
  18. package/lib/cli.js +61 -2
  19. package/lib/datasource-deploy.js +14 -20
  20. package/lib/external-system-deploy.js +123 -40
  21. package/lib/external-system-download.js +431 -0
  22. package/lib/external-system-generator.js +13 -10
  23. package/lib/external-system-test.js +446 -0
  24. package/lib/generator-builders.js +323 -0
  25. package/lib/generator.js +200 -292
  26. package/lib/schema/application-schema.json +853 -852
  27. package/lib/schema/external-datasource.schema.json +823 -49
  28. package/lib/schema/external-system.schema.json +96 -78
  29. package/lib/templates.js +1 -1
  30. package/lib/utils/cli-utils.js +4 -4
  31. package/lib/utils/external-system-display.js +159 -0
  32. package/lib/utils/external-system-validators.js +245 -0
  33. package/lib/utils/paths.js +151 -1
  34. package/lib/utils/schema-resolver.js +7 -2
  35. package/lib/validator.js +5 -2
  36. package/package.json +1 -1
@@ -0,0 +1,431 @@
1
+ /**
2
+ * External System Download Module
3
+ *
4
+ * Downloads external systems from dataplane to local development structure.
5
+ * Supports downloading system configuration and datasources for local development.
6
+ *
7
+ * @fileoverview External system download functionality for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const yaml = require('js-yaml');
16
+ const chalk = require('chalk');
17
+ const { authenticatedApiCall } = require('./utils/api');
18
+ const { getDeploymentAuth } = require('./utils/token-manager');
19
+ const { getDataplaneUrl } = require('./datasource-deploy');
20
+ const { getConfig } = require('./config');
21
+ const { detectAppType } = require('./utils/paths');
22
+ const logger = require('./utils/logger');
23
+
24
+ /**
25
+ * Validates system type from downloaded application
26
+ * @param {Object} application - External system configuration
27
+ * @returns {string} System type (openapi, mcp, custom)
28
+ * @throws {Error} If system type is invalid
29
+ */
30
+ function validateSystemType(application) {
31
+ if (!application || typeof application !== 'object') {
32
+ throw new Error('Application configuration is required');
33
+ }
34
+
35
+ const validTypes = ['openapi', 'mcp', 'custom'];
36
+ const systemType = application.type;
37
+
38
+ if (!systemType || !validTypes.includes(systemType)) {
39
+ throw new Error(`Invalid system type: ${systemType}. Must be one of: ${validTypes.join(', ')}`);
40
+ }
41
+
42
+ return systemType;
43
+ }
44
+
45
+ /**
46
+ * Validates downloaded data structure before writing files
47
+ * @param {Object} application - External system configuration
48
+ * @param {Array} dataSources - Array of datasource configurations
49
+ * @throws {Error} If validation fails
50
+ */
51
+ function validateDownloadedData(application, dataSources) {
52
+ if (!application || typeof application !== 'object') {
53
+ throw new Error('Application configuration is required');
54
+ }
55
+
56
+ if (!application.key || typeof application.key !== 'string') {
57
+ throw new Error('Application key is required');
58
+ }
59
+
60
+ if (!Array.isArray(dataSources)) {
61
+ throw new Error('DataSources must be an array');
62
+ }
63
+
64
+ // Validate each datasource has required fields
65
+ for (const datasource of dataSources) {
66
+ if (!datasource.key || typeof datasource.key !== 'string') {
67
+ throw new Error('Datasource key is required for all datasources');
68
+ }
69
+ if (!datasource.systemKey || typeof datasource.systemKey !== 'string') {
70
+ throw new Error('Datasource systemKey is required for all datasources');
71
+ }
72
+ if (datasource.systemKey !== application.key) {
73
+ throw new Error(`Datasource systemKey (${datasource.systemKey}) does not match application key (${application.key})`);
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Handles partial download errors gracefully
80
+ * @param {string} systemKey - System key
81
+ * @param {Object} systemData - System data that was successfully downloaded
82
+ * @param {Array<Error>} datasourceErrors - Array of errors from datasource downloads
83
+ * @throws {Error} Aggregated error message
84
+ */
85
+ function handlePartialDownload(systemKey, systemData, datasourceErrors) {
86
+ if (datasourceErrors.length === 0) {
87
+ return;
88
+ }
89
+
90
+ const errorMessages = datasourceErrors.map(err => err.message).join('\n - ');
91
+ throw new Error(
92
+ `Partial download completed for system '${systemKey}', but some datasources failed:\n - ${errorMessages}\n\n` +
93
+ 'System configuration was downloaded successfully. You may need to download datasources separately.'
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Extracts environment variables from authentication configuration
99
+ * @param {Object} application - External system configuration
100
+ * @returns {string} Environment variables template content
101
+ */
102
+ function generateEnvTemplate(application) {
103
+ const lines = ['# Environment variables for external system'];
104
+ lines.push(`# System: ${application.key || 'unknown'}`);
105
+ lines.push('');
106
+
107
+ if (!application.authentication) {
108
+ return lines.join('\n');
109
+ }
110
+
111
+ const auth = application.authentication;
112
+
113
+ // OAuth2 configuration
114
+ if (auth.type === 'oauth2' && auth.oauth2) {
115
+ if (auth.oauth2.clientId && auth.oauth2.clientId.includes('{{')) {
116
+ const key = auth.oauth2.clientId.replace(/[{}]/g, '').trim();
117
+ lines.push(`${key}=kv://secrets/${application.key}/client-id`);
118
+ }
119
+ if (auth.oauth2.clientSecret && auth.oauth2.clientSecret.includes('{{')) {
120
+ const key = auth.oauth2.clientSecret.replace(/[{}]/g, '').trim();
121
+ lines.push(`${key}=kv://secrets/${application.key}/client-secret`);
122
+ }
123
+ }
124
+
125
+ // API Key configuration
126
+ if (auth.type === 'apikey' && auth.apikey) {
127
+ if (auth.apikey.key && auth.apikey.key.includes('{{')) {
128
+ const key = auth.apikey.key.replace(/[{}]/g, '').trim();
129
+ lines.push(`${key}=kv://secrets/${application.key}/api-key`);
130
+ }
131
+ }
132
+
133
+ // Basic Auth configuration
134
+ if (auth.type === 'basic' && auth.basic) {
135
+ if (auth.basic.username && auth.basic.username.includes('{{')) {
136
+ const key = auth.basic.username.replace(/[{}]/g, '').trim();
137
+ lines.push(`${key}=kv://secrets/${application.key}/username`);
138
+ }
139
+ if (auth.basic.password && auth.basic.password.includes('{{')) {
140
+ const key = auth.basic.password.replace(/[{}]/g, '').trim();
141
+ lines.push(`${key}=kv://secrets/${application.key}/password`);
142
+ }
143
+ }
144
+
145
+ return lines.join('\n');
146
+ }
147
+
148
+ /**
149
+ * Generates variables.yaml with externalIntegration block
150
+ * @param {string} systemKey - System key
151
+ * @param {Object} application - External system configuration
152
+ * @param {Array} dataSources - Array of datasource configurations
153
+ * @returns {Object} Variables YAML object
154
+ */
155
+ function generateVariablesYaml(systemKey, application, dataSources) {
156
+ const systemFileName = `${systemKey}-deploy.json`;
157
+ const datasourceFiles = dataSources.map(ds => {
158
+ // Extract entity key from datasource key or use entityKey
159
+ const entityKey = ds.entityKey || ds.key.split('-').pop();
160
+ return `${systemKey}-deploy-${entityKey}.json`;
161
+ });
162
+
163
+ return {
164
+ name: systemKey,
165
+ displayName: application.displayName || systemKey,
166
+ description: application.description || `External system integration for ${systemKey}`,
167
+ externalIntegration: {
168
+ schemaBasePath: './',
169
+ systems: [systemFileName],
170
+ dataSources: datasourceFiles,
171
+ autopublish: false,
172
+ version: application.version || '1.0.0'
173
+ }
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Generates README.md with setup instructions
179
+ * @param {string} systemKey - System key
180
+ * @param {Object} application - External system configuration
181
+ * @param {Array} dataSources - Array of datasource configurations
182
+ * @returns {string} README.md content
183
+ */
184
+ function generateReadme(systemKey, application, dataSources) {
185
+ const displayName = application.displayName || systemKey;
186
+ const description = application.description || `External system integration for ${systemKey}`;
187
+ const systemType = application.type || 'unknown';
188
+
189
+ const lines = [
190
+ `# ${displayName}`,
191
+ '',
192
+ description,
193
+ '',
194
+ '## System Information',
195
+ '',
196
+ `- **System Key**: \`${systemKey}\``,
197
+ `- **System Type**: \`${systemType}\``,
198
+ `- **Datasources**: ${dataSources.length}`,
199
+ '',
200
+ '## Files',
201
+ '',
202
+ '- `variables.yaml` - Application configuration with externalIntegration block',
203
+ `- \`${systemKey}-deploy.json\` - External system definition`
204
+ ];
205
+
206
+ dataSources.forEach(ds => {
207
+ const entityKey = ds.entityKey || ds.key.split('-').pop();
208
+ lines.push(`- \`${systemKey}-deploy-${entityKey}.json\` - Datasource: ${ds.displayName || ds.key}`);
209
+ });
210
+
211
+ lines.push(
212
+ '- `env.template` - Environment variables template',
213
+ '',
214
+ '## Setup Instructions',
215
+ '',
216
+ '1. Review and update configuration files as needed',
217
+ '2. Set up environment variables in `env.template`',
218
+ '3. Run unit tests: `aifabrix test ${systemKey}`',
219
+ '4. Run integration tests: `aifabrix test-integration ${systemKey}`',
220
+ '5. Deploy: `aifabrix deploy ${systemKey} --environment dev`',
221
+ '',
222
+ '## Testing',
223
+ '',
224
+ '### Unit Tests',
225
+ 'Run local validation without API calls:',
226
+ '```bash',
227
+ `aifabrix test ${systemKey}`,
228
+ '```',
229
+ '',
230
+ '### Integration Tests',
231
+ 'Run integration tests via dataplane:',
232
+ '```bash',
233
+ `aifabrix test-integration ${systemKey} --environment dev`,
234
+ '```',
235
+ '',
236
+ '## Deployment',
237
+ '',
238
+ 'Deploy to dataplane via miso-controller:',
239
+ '```bash',
240
+ `aifabrix deploy ${systemKey} --environment dev`,
241
+ '```'
242
+ );
243
+
244
+ return lines.join('\n');
245
+ }
246
+
247
+ /**
248
+ * Downloads external system from dataplane to local development structure
249
+ * @async
250
+ * @function downloadExternalSystem
251
+ * @param {string} systemKey - System key or ID
252
+ * @param {Object} options - Download options
253
+ * @param {string} [options.environment] - Environment (dev, tst, pro)
254
+ * @param {string} [options.controller] - Controller URL
255
+ * @param {boolean} [options.dryRun] - Show what would be downloaded without actually downloading
256
+ * @returns {Promise<void>} Resolves when download completes
257
+ * @throws {Error} If download fails
258
+ */
259
+ async function downloadExternalSystem(systemKey, options = {}) {
260
+ if (!systemKey || typeof systemKey !== 'string') {
261
+ throw new Error('System key is required and must be a string');
262
+ }
263
+
264
+ // Validate system key format (alphanumeric, hyphens, underscores)
265
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
266
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
267
+ }
268
+
269
+ try {
270
+ logger.log(chalk.blue(`\nšŸ“„ Downloading external system: ${systemKey}`));
271
+
272
+ // Get authentication
273
+ const config = await getConfig();
274
+ const environment = options.environment || 'dev';
275
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
276
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
277
+
278
+ if (!authConfig.token && !authConfig.clientId) {
279
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
280
+ }
281
+
282
+ // Get dataplane URL from controller
283
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
284
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
285
+ logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
286
+
287
+ // Download system configuration
288
+ // Note: Verify this endpoint exists. Alternative: GET /api/v1/pipeline/{systemIdOrKey}
289
+ const downloadEndpoint = `${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`;
290
+ logger.log(chalk.blue(`šŸ“” Downloading from: ${downloadEndpoint}`));
291
+
292
+ if (options.dryRun) {
293
+ logger.log(chalk.yellow('šŸ” Dry run mode - would download from:'));
294
+ logger.log(chalk.gray(` ${downloadEndpoint}`));
295
+ logger.log(chalk.yellow('\nWould create:'));
296
+ logger.log(chalk.gray(` integration/${systemKey}/`));
297
+ logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
298
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
299
+ logger.log(chalk.gray(` integration/${systemKey}/env.template`));
300
+ logger.log(chalk.gray(` integration/${systemKey}/README.md`));
301
+ return;
302
+ }
303
+
304
+ const response = await authenticatedApiCall(
305
+ downloadEndpoint,
306
+ {
307
+ method: 'GET'
308
+ },
309
+ authConfig.token
310
+ );
311
+
312
+ if (!response.success || !response.data) {
313
+ throw new Error(`Failed to download system configuration: ${response.error || response.formattedError || 'Unknown error'}`);
314
+ }
315
+
316
+ const downloadData = response.data.data || response.data;
317
+ const application = downloadData.application;
318
+ const dataSources = downloadData.dataSources || [];
319
+
320
+ if (!application) {
321
+ throw new Error('Application configuration not found in download response');
322
+ }
323
+
324
+ // Validate downloaded data
325
+ logger.log(chalk.blue('šŸ” Validating downloaded data...'));
326
+ validateDownloadedData(application, dataSources);
327
+ const systemType = validateSystemType(application);
328
+ logger.log(chalk.green(`āœ“ System type: ${systemType}`));
329
+ logger.log(chalk.green(`āœ“ Found ${dataSources.length} datasource(s)`));
330
+
331
+ // Create temporary folder for validation
332
+ const tempDir = path.join(os.tmpdir(), `aifabrix-download-${systemKey}-${Date.now()}`);
333
+ await fs.mkdir(tempDir, { recursive: true });
334
+
335
+ try {
336
+ // Generate files in temporary folder first
337
+ const systemFileName = `${systemKey}-deploy.json`;
338
+ const systemFilePath = path.join(tempDir, systemFileName);
339
+ await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
340
+
341
+ // Generate datasource files
342
+ const datasourceErrors = [];
343
+ const datasourceFiles = [];
344
+ for (const datasource of dataSources) {
345
+ try {
346
+ const entityKey = datasource.entityKey || datasource.key.split('-').pop();
347
+ const datasourceFileName = `${systemKey}-deploy-${entityKey}.json`;
348
+ const datasourceFilePath = path.join(tempDir, datasourceFileName);
349
+ await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
350
+ datasourceFiles.push(datasourceFilePath);
351
+ } catch (error) {
352
+ datasourceErrors.push(new Error(`Failed to write datasource ${datasource.key}: ${error.message}`));
353
+ }
354
+ }
355
+
356
+ // Handle partial downloads
357
+ if (datasourceErrors.length > 0) {
358
+ handlePartialDownload(systemKey, application, datasourceErrors);
359
+ }
360
+
361
+ // Generate variables.yaml
362
+ const variables = generateVariablesYaml(systemKey, application, dataSources);
363
+ const variablesPath = path.join(tempDir, 'variables.yaml');
364
+ await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
365
+
366
+ // Generate env.template
367
+ const envTemplate = generateEnvTemplate(application);
368
+ const envTemplatePath = path.join(tempDir, 'env.template');
369
+ await fs.writeFile(envTemplatePath, envTemplate, 'utf8');
370
+
371
+ // Generate README.md
372
+ const readme = generateReadme(systemKey, application, dataSources);
373
+ const readmePath = path.join(tempDir, 'README.md');
374
+ await fs.writeFile(readmePath, readme, 'utf8');
375
+
376
+ // Determine final destination (integration folder)
377
+ const { appPath } = await detectAppType(systemKey);
378
+ const finalPath = appPath || path.join(process.cwd(), 'integration', systemKey);
379
+
380
+ // Create final directory
381
+ await fs.mkdir(finalPath, { recursive: true });
382
+
383
+ // Move files from temp to final location
384
+ logger.log(chalk.blue(`šŸ“ Creating directory: ${finalPath}`));
385
+ const filesToMove = [
386
+ { from: systemFilePath, to: path.join(finalPath, systemFileName) },
387
+ { from: variablesPath, to: path.join(finalPath, 'variables.yaml') },
388
+ { from: envTemplatePath, to: path.join(finalPath, 'env.template') },
389
+ { from: readmePath, to: path.join(finalPath, 'README.md') }
390
+ ];
391
+
392
+ for (const dsFile of datasourceFiles) {
393
+ const fileName = path.basename(dsFile);
394
+ filesToMove.push({ from: dsFile, to: path.join(finalPath, fileName) });
395
+ }
396
+
397
+ for (const file of filesToMove) {
398
+ await fs.copyFile(file.from, file.to);
399
+ logger.log(chalk.green(`āœ“ Created: ${path.relative(process.cwd(), file.to)}`));
400
+ }
401
+
402
+ // Clean up temporary folder
403
+ await fs.rm(tempDir, { recursive: true, force: true });
404
+
405
+ logger.log(chalk.green('\nāœ… External system downloaded successfully!'));
406
+ logger.log(chalk.blue(`Location: ${finalPath}`));
407
+ logger.log(chalk.blue(`System: ${systemKey}`));
408
+ logger.log(chalk.blue(`Datasources: ${dataSources.length}`));
409
+ } catch (error) {
410
+ // Clean up temporary folder on error
411
+ try {
412
+ await fs.rm(tempDir, { recursive: true, force: true });
413
+ } catch {
414
+ // Ignore cleanup errors
415
+ }
416
+ throw error;
417
+ }
418
+ } catch (error) {
419
+ throw new Error(`Failed to download external system: ${error.message}`);
420
+ }
421
+ }
422
+
423
+ module.exports = {
424
+ downloadExternalSystem,
425
+ validateSystemType,
426
+ validateDownloadedData,
427
+ generateVariablesYaml,
428
+ generateEnvTemplate,
429
+ generateReadme,
430
+ handlePartialDownload
431
+ };
@@ -44,10 +44,10 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
44
44
  };
45
45
 
46
46
  const rendered = template(context);
47
- const schemasDir = path.join(appPath, 'schemas');
48
- await fs.mkdir(schemasDir, { recursive: true });
49
47
 
50
- const outputPath = path.join(schemasDir, `${systemKey}.json`);
48
+ // Generate in same folder as variables.yaml (new structure)
49
+ // Use naming: <app-name>-deploy.json
50
+ const outputPath = path.join(appPath, `${systemKey}-deploy.json`);
51
51
  await fs.writeFile(outputPath, rendered, 'utf8');
52
52
 
53
53
  return outputPath;
@@ -83,10 +83,10 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
83
83
  };
84
84
 
85
85
  const rendered = template(context);
86
- const schemasDir = path.join(appPath, 'schemas');
87
- await fs.mkdir(schemasDir, { recursive: true });
88
86
 
89
- const outputPath = path.join(schemasDir, `${datasourceKey}.json`);
87
+ // Generate in same folder as variables.yaml (new structure)
88
+ // Use naming: <app-name>-deploy-<datasource-key>.json
89
+ const outputPath = path.join(appPath, `${datasourceKey}-deploy.json`);
90
90
  await fs.writeFile(outputPath, rendered, 'utf8');
91
91
 
92
92
  return outputPath;
@@ -120,7 +120,8 @@ async function generateExternalSystemFiles(appPath, appName, config) {
120
120
 
121
121
  for (let i = 0; i < datasourceCount; i++) {
122
122
  const entityKey = `entity${i + 1}`;
123
- const datasourceKey = `${systemKey}-${entityKey}`;
123
+ // For datasource key, use just the entity key (will be prefixed with app-name-deploy-)
124
+ const datasourceKey = entityKey;
124
125
  const resourceType = resourceTypes[i % resourceTypes.length];
125
126
 
126
127
  const datasourceConfig = {
@@ -132,7 +133,8 @@ async function generateExternalSystemFiles(appPath, appName, config) {
132
133
  datasourceDescription: `External datasource for ${entityKey} entity`
133
134
  };
134
135
 
135
- const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig);
136
+ // Generate with full naming: <app-name>-deploy-<entity-key>.json
137
+ const datasourcePath = await generateExternalDataSourceTemplate(appPath, `${systemKey}-deploy-${datasourceKey}`, datasourceConfig);
136
138
  datasourcePaths.push(datasourcePath);
137
139
  logger.log(chalk.green(`āœ“ Generated datasource: ${path.basename(datasourcePath)}`));
138
140
  }
@@ -165,9 +167,10 @@ async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, da
165
167
  const variables = yaml.load(variablesContent);
166
168
 
167
169
  // Add externalIntegration block
170
+ // Files are in same folder, so schemaBasePath is './'
168
171
  variables.externalIntegration = {
169
- schemaBasePath: './schemas',
170
- systems: [`${systemKey}.json`],
172
+ schemaBasePath: './',
173
+ systems: [`${systemKey}-deploy.json`],
171
174
  dataSources: datasourcePaths.map(p => path.basename(p)),
172
175
  autopublish: true,
173
176
  version: '1.0.0'