@aifabrix/builder 2.7.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 (47) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/integration/hubspot/README.md +136 -0
  3. package/integration/hubspot/env.template +9 -0
  4. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  5. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  6. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  7. package/integration/hubspot/hubspot-deploy.json +91 -0
  8. package/integration/hubspot/variables.yaml +17 -0
  9. package/lib/app-config.js +13 -2
  10. package/lib/app-deploy.js +9 -3
  11. package/lib/app-dockerfile.js +14 -1
  12. package/lib/app-prompts.js +177 -13
  13. package/lib/app-push.js +16 -1
  14. package/lib/app-register.js +37 -5
  15. package/lib/app-rotate-secret.js +10 -0
  16. package/lib/app-run.js +19 -0
  17. package/lib/app.js +70 -25
  18. package/lib/audit-logger.js +9 -4
  19. package/lib/build.js +25 -13
  20. package/lib/cli.js +109 -2
  21. package/lib/commands/login.js +40 -3
  22. package/lib/config.js +121 -114
  23. package/lib/datasource-deploy.js +14 -20
  24. package/lib/environment-deploy.js +305 -0
  25. package/lib/external-system-deploy.js +345 -0
  26. package/lib/external-system-download.js +431 -0
  27. package/lib/external-system-generator.js +190 -0
  28. package/lib/external-system-test.js +446 -0
  29. package/lib/generator-builders.js +323 -0
  30. package/lib/generator.js +200 -292
  31. package/lib/schema/application-schema.json +830 -800
  32. package/lib/schema/external-datasource.schema.json +868 -46
  33. package/lib/schema/external-system.schema.json +98 -80
  34. package/lib/schema/infrastructure-schema.json +1 -1
  35. package/lib/templates.js +32 -1
  36. package/lib/utils/cli-utils.js +4 -4
  37. package/lib/utils/device-code.js +10 -2
  38. package/lib/utils/external-system-display.js +159 -0
  39. package/lib/utils/external-system-validators.js +245 -0
  40. package/lib/utils/paths.js +151 -1
  41. package/lib/utils/schema-resolver.js +7 -2
  42. package/lib/utils/token-encryption.js +68 -0
  43. package/lib/validator.js +52 -5
  44. package/package.json +1 -1
  45. package/tatus +181 -0
  46. package/templates/external-system/external-datasource.json.hbs +55 -0
  47. package/templates/external-system/external-system.json.hbs +37 -0
@@ -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
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * External System Template Generation Module
3
+ *
4
+ * Generates external system and datasource JSON files from Handlebars templates
5
+ * for external type applications.
6
+ *
7
+ * @fileoverview External system template generation 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 handlebars = require('handlebars');
15
+ const yaml = require('js-yaml');
16
+ const chalk = require('chalk');
17
+ const logger = require('./utils/logger');
18
+
19
+ // Register Handlebars helper for equality check
20
+ handlebars.registerHelper('eq', (a, b) => a === b);
21
+
22
+ /**
23
+ * Generates external system JSON file from template
24
+ * @async
25
+ * @function generateExternalSystemTemplate
26
+ * @param {string} appPath - Application directory path
27
+ * @param {string} systemKey - System key
28
+ * @param {Object} config - System configuration
29
+ * @returns {Promise<string>} Path to generated file
30
+ * @throws {Error} If generation fails
31
+ */
32
+ async function generateExternalSystemTemplate(appPath, systemKey, config) {
33
+ try {
34
+ const templatePath = path.join(__dirname, '..', 'templates', 'external-system', 'external-system.json.hbs');
35
+ const templateContent = await fs.readFile(templatePath, 'utf8');
36
+ const template = handlebars.compile(templateContent);
37
+
38
+ const context = {
39
+ systemKey: systemKey,
40
+ systemDisplayName: config.systemDisplayName || systemKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
41
+ systemDescription: config.systemDescription || `External system integration for ${systemKey}`,
42
+ systemType: config.systemType || 'openapi',
43
+ authType: config.authType || 'apikey'
44
+ };
45
+
46
+ const rendered = template(context);
47
+
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
+ await fs.writeFile(outputPath, rendered, 'utf8');
52
+
53
+ return outputPath;
54
+ } catch (error) {
55
+ throw new Error(`Failed to generate external system template: ${error.message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Generates external datasource JSON file from template
61
+ * @async
62
+ * @function generateExternalDataSourceTemplate
63
+ * @param {string} appPath - Application directory path
64
+ * @param {string} datasourceKey - Datasource key
65
+ * @param {Object} config - Datasource configuration
66
+ * @returns {Promise<string>} Path to generated file
67
+ * @throws {Error} If generation fails
68
+ */
69
+ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config) {
70
+ try {
71
+ const templatePath = path.join(__dirname, '..', 'templates', 'external-system', 'external-datasource.json.hbs');
72
+ const templateContent = await fs.readFile(templatePath, 'utf8');
73
+ const template = handlebars.compile(templateContent);
74
+
75
+ const context = {
76
+ datasourceKey: datasourceKey,
77
+ datasourceDisplayName: config.datasourceDisplayName || datasourceKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
78
+ datasourceDescription: config.datasourceDescription || `External datasource for ${datasourceKey}`,
79
+ systemKey: config.systemKey,
80
+ entityKey: config.entityKey || datasourceKey.split('-').pop(),
81
+ resourceType: config.resourceType || 'document',
82
+ systemType: config.systemType || 'openapi'
83
+ };
84
+
85
+ const rendered = template(context);
86
+
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
+ await fs.writeFile(outputPath, rendered, 'utf8');
91
+
92
+ return outputPath;
93
+ } catch (error) {
94
+ throw new Error(`Failed to generate external datasource template: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Generates all external system files (system + datasources)
100
+ * @async
101
+ * @function generateExternalSystemFiles
102
+ * @param {string} appPath - Application directory path
103
+ * @param {string} appName - Application name
104
+ * @param {Object} config - Configuration with external system details
105
+ * @returns {Promise<Object>} Object with system and datasource file paths
106
+ * @throws {Error} If generation fails
107
+ */
108
+ async function generateExternalSystemFiles(appPath, appName, config) {
109
+ try {
110
+ const systemKey = config.systemKey || appName;
111
+ const datasourceCount = config.datasourceCount || 1;
112
+
113
+ // Generate external system JSON
114
+ const systemPath = await generateExternalSystemTemplate(appPath, systemKey, config);
115
+ logger.log(chalk.green(`✓ Generated external system: ${path.basename(systemPath)}`));
116
+
117
+ // Generate datasource JSON files
118
+ const datasourcePaths = [];
119
+ const resourceTypes = ['customer', 'contact', 'person', 'document', 'deal'];
120
+
121
+ for (let i = 0; i < datasourceCount; i++) {
122
+ const entityKey = `entity${i + 1}`;
123
+ // For datasource key, use just the entity key (will be prefixed with app-name-deploy-)
124
+ const datasourceKey = entityKey;
125
+ const resourceType = resourceTypes[i % resourceTypes.length];
126
+
127
+ const datasourceConfig = {
128
+ systemKey: systemKey,
129
+ entityKey: entityKey,
130
+ resourceType: resourceType,
131
+ systemType: config.systemType || 'openapi',
132
+ datasourceDisplayName: `${config.systemDisplayName || systemKey} ${entityKey}`,
133
+ datasourceDescription: `External datasource for ${entityKey} entity`
134
+ };
135
+
136
+ // Generate with full naming: <app-name>-deploy-<entity-key>.json
137
+ const datasourcePath = await generateExternalDataSourceTemplate(appPath, `${systemKey}-deploy-${datasourceKey}`, datasourceConfig);
138
+ datasourcePaths.push(datasourcePath);
139
+ logger.log(chalk.green(`✓ Generated datasource: ${path.basename(datasourcePath)}`));
140
+ }
141
+
142
+ // Update variables.yaml with externalIntegration block
143
+ await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths);
144
+
145
+ return {
146
+ systemPath,
147
+ datasourcePaths
148
+ };
149
+ } catch (error) {
150
+ throw new Error(`Failed to generate external system files: ${error.message}`);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Updates variables.yaml with externalIntegration block
156
+ * @async
157
+ * @function updateVariablesYamlWithExternalIntegration
158
+ * @param {string} appPath - Application directory path
159
+ * @param {string} systemKey - System key
160
+ * @param {Array<string>} datasourcePaths - Array of datasource file paths
161
+ * @throws {Error} If update fails
162
+ */
163
+ async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths) {
164
+ try {
165
+ const variablesPath = path.join(appPath, 'variables.yaml');
166
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
167
+ const variables = yaml.load(variablesContent);
168
+
169
+ // Add externalIntegration block
170
+ // Files are in same folder, so schemaBasePath is './'
171
+ variables.externalIntegration = {
172
+ schemaBasePath: './',
173
+ systems: [`${systemKey}-deploy.json`],
174
+ dataSources: datasourcePaths.map(p => path.basename(p)),
175
+ autopublish: true,
176
+ version: '1.0.0'
177
+ };
178
+
179
+ await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
180
+ } catch (error) {
181
+ throw new Error(`Failed to update variables.yaml: ${error.message}`);
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ generateExternalSystemTemplate,
187
+ generateExternalDataSourceTemplate,
188
+ generateExternalSystemFiles
189
+ };
190
+