@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.
- package/.cursor/rules/project-rules.mdc +680 -0
- package/integration/hubspot/README.md +136 -0
- package/integration/hubspot/env.template +9 -0
- package/integration/hubspot/hubspot-deploy-company.json +200 -0
- package/integration/hubspot/hubspot-deploy-contact.json +228 -0
- package/integration/hubspot/hubspot-deploy-deal.json +248 -0
- package/integration/hubspot/hubspot-deploy.json +91 -0
- package/integration/hubspot/variables.yaml +17 -0
- package/lib/app-config.js +13 -2
- package/lib/app-deploy.js +9 -3
- package/lib/app-dockerfile.js +14 -1
- package/lib/app-prompts.js +177 -13
- package/lib/app-push.js +16 -1
- package/lib/app-register.js +37 -5
- package/lib/app-rotate-secret.js +10 -0
- package/lib/app-run.js +19 -0
- package/lib/app.js +70 -25
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +25 -13
- package/lib/cli.js +109 -2
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +14 -20
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +345 -0
- package/lib/external-system-download.js +431 -0
- package/lib/external-system-generator.js +190 -0
- package/lib/external-system-test.js +446 -0
- package/lib/generator-builders.js +323 -0
- package/lib/generator.js +200 -292
- package/lib/schema/application-schema.json +830 -800
- package/lib/schema/external-datasource.schema.json +868 -46
- package/lib/schema/external-system.schema.json +98 -80
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/templates.js +32 -1
- package/lib/utils/cli-utils.js +4 -4
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/external-system-display.js +159 -0
- package/lib/utils/external-system-validators.js +245 -0
- package/lib/utils/paths.js +151 -1
- package/lib/utils/schema-resolver.js +7 -2
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validator.js +52 -5
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- 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
|
+
|