@aifabrix/builder 2.22.2 → 2.31.1

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 (63) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +210 -80
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +7 -5
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/scripts/install-local.js +210 -0
  61. package/templates/applications/miso-controller/test.yaml +1 -0
  62. package/templates/python/Dockerfile.hbs +8 -45
  63. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -14,7 +14,8 @@ const fsSync = require('fs');
14
14
  const path = require('path');
15
15
  const yaml = require('js-yaml');
16
16
  const chalk = require('chalk');
17
- const { testDatasourceViaPipeline } = require('./api/pipeline.api');
17
+ const testHelpers = require('./utils/external-system-test-helpers');
18
+ const { retryApiCall } = require('./utils/external-system-test-helpers');
18
19
  const { getDeploymentAuth } = require('./utils/token-manager');
19
20
  const { getDataplaneUrl } = require('./datasource-deploy');
20
21
  const { getConfig } = require('./config');
@@ -115,6 +116,144 @@ async function loadExternalSystemFiles(appName) {
115
116
  };
116
117
  }
117
118
 
119
+ /**
120
+ * Validate system files against schema
121
+ * @param {Array} systemFiles - Array of system file objects
122
+ * @param {Object} externalSystemSchema - External system schema
123
+ * @returns {Object} Validation results
124
+ */
125
+ function validateSystemFiles(systemFiles, externalSystemSchema) {
126
+ const systemResults = [];
127
+ let valid = true;
128
+ const errors = [];
129
+
130
+ for (const systemFile of systemFiles) {
131
+ const validation = validateAgainstSchema(systemFile.data, externalSystemSchema);
132
+ if (!validation.valid) {
133
+ valid = false;
134
+ errors.push(`System file ${path.basename(systemFile.path)}: ${validation.errors.join(', ')}`);
135
+ } else {
136
+ systemResults.push({
137
+ file: path.basename(systemFile.path),
138
+ valid: true
139
+ });
140
+ }
141
+ }
142
+
143
+ return { valid, errors, systemResults };
144
+ }
145
+
146
+ /**
147
+ * Validate datasource against schema and relationships
148
+ * @param {Object} datasource - Datasource configuration
149
+ * @param {string} systemKey - System key
150
+ * @param {Object} externalDataSourceSchema - External datasource schema
151
+ * @returns {Object} Validation result
152
+ */
153
+ function validateDatasourceSchema(datasource, systemKey, externalDataSourceSchema) {
154
+ const errors = [];
155
+ const schemaValidation = validateAgainstSchema(datasource, externalDataSourceSchema);
156
+ if (!schemaValidation.valid) {
157
+ errors.push(...schemaValidation.errors);
158
+ }
159
+
160
+ if (systemKey && datasource.systemKey !== systemKey) {
161
+ errors.push(`systemKey mismatch: expected '${systemKey}', got '${datasource.systemKey}'`);
162
+ }
163
+
164
+ return {
165
+ valid: errors.length === 0,
166
+ errors
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Test datasource with payload template
172
+ * @param {Object} datasource - Datasource configuration
173
+ * @param {boolean} verbose - Show detailed output
174
+ * @returns {Object} Test results
175
+ */
176
+ function testDatasourceWithPayload(datasource, verbose) {
177
+ const errors = [];
178
+ const warnings = [];
179
+
180
+ // Validate field mappings
181
+ const fieldMappingResults = validateFieldMappings(datasource, datasource.testPayload);
182
+ if (!fieldMappingResults.valid) {
183
+ errors.push(...fieldMappingResults.errors);
184
+ }
185
+ if (fieldMappingResults.warnings.length > 0) {
186
+ warnings.push(...fieldMappingResults.warnings);
187
+ }
188
+
189
+ // Validate metadata schema
190
+ const metadataSchemaResults = validateMetadataSchema(datasource, datasource.testPayload);
191
+ if (!metadataSchemaResults.valid) {
192
+ errors.push(...metadataSchemaResults.errors);
193
+ }
194
+ if (metadataSchemaResults.warnings.length > 0) {
195
+ warnings.push(...metadataSchemaResults.warnings);
196
+ }
197
+
198
+ // Compare with expectedResult if provided
199
+ if (datasource.testPayload.expectedResult && fieldMappingResults.mappedFields && verbose) {
200
+ warnings.push('expectedResult validation not yet implemented (requires transformation engine)');
201
+ }
202
+
203
+ return {
204
+ fieldMappingResults,
205
+ metadataSchemaResults,
206
+ errors,
207
+ warnings,
208
+ valid: errors.length === 0
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Validate a single datasource
214
+ * @param {Object} datasourceFile - Datasource file object
215
+ * @param {string} systemKey - System key
216
+ * @param {Object} externalDataSourceSchema - External datasource schema
217
+ * @param {boolean} verbose - Show detailed output
218
+ * @returns {Object} Datasource validation result
219
+ */
220
+ function validateSingleDatasource(datasourceFile, systemKey, externalDataSourceSchema, verbose) {
221
+ const datasource = datasourceFile.data;
222
+ const datasourceResult = {
223
+ key: datasource.key,
224
+ file: path.basename(datasourceFile.path),
225
+ valid: true,
226
+ errors: [],
227
+ warnings: [],
228
+ fieldMappingResults: null,
229
+ metadataSchemaResults: null
230
+ };
231
+
232
+ // Validate against schema
233
+ const schemaValidation = validateDatasourceSchema(datasource, systemKey, externalDataSourceSchema);
234
+ if (!schemaValidation.valid) {
235
+ datasourceResult.valid = false;
236
+ datasourceResult.errors.push(...schemaValidation.errors);
237
+ }
238
+
239
+ // Test with testPayload if available
240
+ if (datasource.testPayload && datasource.testPayload.payloadTemplate) {
241
+ logger.log(chalk.blue(` Testing datasource: ${datasource.key}`));
242
+ const payloadTestResults = testDatasourceWithPayload(datasource, verbose);
243
+ datasourceResult.fieldMappingResults = payloadTestResults.fieldMappingResults;
244
+ datasourceResult.metadataSchemaResults = payloadTestResults.metadataSchemaResults;
245
+ if (!payloadTestResults.valid) {
246
+ datasourceResult.valid = false;
247
+ datasourceResult.errors.push(...payloadTestResults.errors);
248
+ }
249
+ datasourceResult.warnings.push(...payloadTestResults.warnings);
250
+ } else {
251
+ datasourceResult.warnings.push('No testPayload.payloadTemplate found - skipping field mapping and metadata schema tests');
252
+ }
253
+
254
+ return datasourceResult;
255
+ }
256
+
118
257
  /**
119
258
  * Runs unit tests for external system (local validation, no API calls)
120
259
  * @async
@@ -147,95 +286,28 @@ async function testExternalSystem(appName, options = {}) {
147
286
 
148
287
  // Validate system files
149
288
  logger.log(chalk.blue('šŸ“‹ Validating system files...'));
150
- for (const systemFile of systemFiles) {
151
- const validation = validateAgainstSchema(systemFile.data, externalSystemSchema);
152
- if (!validation.valid) {
153
- results.valid = false;
154
- results.errors.push(`System file ${path.basename(systemFile.path)}: ${validation.errors.join(', ')}`);
155
- } else {
156
- results.systemResults.push({
157
- file: path.basename(systemFile.path),
158
- valid: true
159
- });
160
- }
161
- }
289
+ const systemValidation = validateSystemFiles(systemFiles, externalSystemSchema);
290
+ results.valid = systemValidation.valid;
291
+ results.errors.push(...systemValidation.errors);
292
+ results.systemResults = systemValidation.systemResults;
162
293
 
163
294
  // Validate datasource files
164
295
  logger.log(chalk.blue('šŸ“‹ Validating datasource files...'));
165
- const datasourcesToTest = options.datasource
166
- ? datasourceFiles.filter(ds => ds.data.key === options.datasource || path.basename(ds.path).includes(options.datasource))
167
- : datasourceFiles;
296
+ const datasourcesToTest = determineDatasourcesToTest(datasourceFiles, options.datasource);
297
+ const systemKey = systemFiles.length > 0 ? systemFiles[0].data.key : null;
168
298
 
169
299
  for (const datasourceFile of datasourcesToTest) {
170
- const datasource = datasourceFile.data;
171
- const datasourceResult = {
172
- key: datasource.key,
173
- file: path.basename(datasourceFile.path),
174
- valid: true,
175
- errors: [],
176
- warnings: [],
177
- fieldMappingResults: null,
178
- metadataSchemaResults: null
179
- };
180
-
181
- // Validate against schema
182
- const schemaValidation = validateAgainstSchema(datasource, externalDataSourceSchema);
183
- if (!schemaValidation.valid) {
184
- datasourceResult.valid = false;
185
- datasourceResult.errors.push(...schemaValidation.errors);
300
+ const datasourceResult = validateSingleDatasource(
301
+ datasourceFile,
302
+ systemKey,
303
+ externalDataSourceSchema,
304
+ options.verbose
305
+ );
306
+
307
+ if (!datasourceResult.valid) {
186
308
  results.valid = false;
187
309
  }
188
310
 
189
- // Validate relationships
190
- if (systemFiles.length > 0) {
191
- const systemKey = systemFiles[0].data.key;
192
- if (datasource.systemKey !== systemKey) {
193
- datasourceResult.valid = false;
194
- datasourceResult.errors.push(`systemKey mismatch: expected '${systemKey}', got '${datasource.systemKey}'`);
195
- results.valid = false;
196
- }
197
- }
198
-
199
- // Test with testPayload if available
200
- if (datasource.testPayload && datasource.testPayload.payloadTemplate) {
201
- logger.log(chalk.blue(` Testing datasource: ${datasource.key}`));
202
-
203
- // Validate field mappings
204
- const fieldMappingResults = validateFieldMappings(datasource, datasource.testPayload);
205
- datasourceResult.fieldMappingResults = fieldMappingResults;
206
- if (!fieldMappingResults.valid) {
207
- datasourceResult.valid = false;
208
- datasourceResult.errors.push(...fieldMappingResults.errors);
209
- results.valid = false;
210
- }
211
- if (fieldMappingResults.warnings.length > 0) {
212
- datasourceResult.warnings.push(...fieldMappingResults.warnings);
213
- }
214
-
215
- // Validate metadata schema
216
- const metadataSchemaResults = validateMetadataSchema(datasource, datasource.testPayload);
217
- datasourceResult.metadataSchemaResults = metadataSchemaResults;
218
- if (!metadataSchemaResults.valid) {
219
- datasourceResult.valid = false;
220
- datasourceResult.errors.push(...metadataSchemaResults.errors);
221
- results.valid = false;
222
- }
223
- if (metadataSchemaResults.warnings.length > 0) {
224
- datasourceResult.warnings.push(...metadataSchemaResults.warnings);
225
- }
226
-
227
- // Compare with expectedResult if provided
228
- if (datasource.testPayload.expectedResult && fieldMappingResults.mappedFields) {
229
- // This would require actual transformation execution, which is complex
230
- // For now, we just note that expectedResult is present
231
- if (options.verbose) {
232
- datasourceResult.warnings.push('expectedResult validation not yet implemented (requires transformation engine)');
233
- }
234
- }
235
- } else {
236
- datasourceResult.warnings.push('No testPayload.payloadTemplate found - skipping field mapping and metadata schema tests');
237
- }
238
-
239
311
  results.datasourceResults.push(datasourceResult);
240
312
  }
241
313
 
@@ -253,50 +325,49 @@ async function testExternalSystem(appName, options = {}) {
253
325
  * @param {number} backoffMs - Initial backoff in milliseconds
254
326
  * @returns {Promise<*>} Function result
255
327
  */
256
- async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
257
- let lastError;
258
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
259
- try {
260
- return await fn();
261
- } catch (error) {
262
- lastError = error;
263
- if (attempt < maxRetries) {
264
- const delay = backoffMs * Math.pow(2, attempt);
265
- await new Promise(resolve => setTimeout(resolve, delay));
266
- }
267
- }
328
+
329
+ /**
330
+ * Setup authentication and get dataplane URL for integration tests
331
+ * @async
332
+ * @param {string} appName - Application name
333
+ * @param {Object} options - Test options
334
+ * @param {Object} config - Configuration object
335
+ * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
336
+ * @throws {Error} If authentication fails
337
+ */
338
+ async function setupIntegrationTestAuth(appName, options, config) {
339
+ const environment = options.environment || 'dev';
340
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
341
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
342
+
343
+ if (!authConfig.token && !authConfig.clientId) {
344
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
268
345
  }
269
- throw lastError;
346
+
347
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
348
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
349
+ logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
350
+
351
+ return { authConfig, dataplaneUrl };
270
352
  }
271
353
 
272
354
  /**
273
- * Calls pipeline test endpoint using centralized API client
274
- * @async
275
- * @param {string} systemKey - System key
276
- * @param {string} datasourceKey - Datasource key
277
- * @param {Object} payloadTemplate - Test payload template
278
- * @param {string} dataplaneUrl - Dataplane URL
279
- * @param {Object} authConfig - Authentication configuration
280
- * @param {number} timeout - Request timeout in milliseconds
281
- * @returns {Promise<Object>} Test response
355
+ * Determine which datasources to test
356
+ * @param {Array} datasourceFiles - All datasource files
357
+ * @param {string} [datasourceFilter] - Optional datasource filter
358
+ * @returns {Array} Filtered datasource files
359
+ * @throws {Error} If no datasources found
282
360
  */
283
- async function callPipelineTestEndpoint(systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000) {
284
- const response = await retryApiCall(async() => {
285
- return await testDatasourceViaPipeline(
286
- dataplaneUrl,
287
- systemKey,
288
- datasourceKey,
289
- authConfig,
290
- { payloadTemplate },
291
- { timeout }
292
- );
293
- });
294
-
295
- if (!response.success || !response.data) {
296
- throw new Error(`Test endpoint failed: ${response.error || response.formattedError || 'Unknown error'}`);
361
+ function determineDatasourcesToTest(datasourceFiles, datasourceFilter) {
362
+ const datasourcesToTest = datasourceFilter
363
+ ? datasourceFiles.filter(ds => ds.data.key === datasourceFilter || path.basename(ds.path).includes(datasourceFilter))
364
+ : datasourceFiles;
365
+
366
+ if (datasourcesToTest.length === 0) {
367
+ throw new Error('No datasources found to test');
297
368
  }
298
369
 
299
- return response.data.data || response.data;
370
+ return datasourcesToTest;
300
371
  }
301
372
 
302
373
  /**
@@ -331,29 +402,12 @@ async function testExternalSystemIntegration(appName, options = {}) {
331
402
 
332
403
  const systemKey = systemFiles[0].data.key;
333
404
 
334
- // Get authentication
405
+ // Setup authentication and dataplane URL
335
406
  const config = await getConfig();
336
- const environment = options.environment || 'dev';
337
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
338
- const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
339
-
340
- if (!authConfig.token && !authConfig.clientId) {
341
- throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
342
- }
343
-
344
- // Get dataplane URL
345
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
346
- const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
347
- logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
407
+ const { authConfig, dataplaneUrl } = await setupIntegrationTestAuth(appName, options, config);
348
408
 
349
409
  // Determine datasources to test
350
- const datasourcesToTest = options.datasource
351
- ? datasourceFiles.filter(ds => ds.data.key === options.datasource || path.basename(ds.path).includes(options.datasource))
352
- : datasourceFiles;
353
-
354
- if (datasourcesToTest.length === 0) {
355
- throw new Error('No datasources found to test');
356
- }
410
+ const datasourcesToTest = determineDatasourcesToTest(datasourceFiles, options.datasource);
357
411
 
358
412
  const results = {
359
413
  success: true,
@@ -362,12 +416,7 @@ async function testExternalSystemIntegration(appName, options = {}) {
362
416
  };
363
417
 
364
418
  // Load custom payload if provided
365
- let customPayload = null;
366
- if (options.payload) {
367
- const payloadPath = path.isAbsolute(options.payload) ? options.payload : path.join(process.cwd(), options.payload);
368
- const payloadContent = await fs.readFile(payloadPath, 'utf8');
369
- customPayload = JSON.parse(payloadContent);
370
- }
419
+ const customPayload = await testHelpers.loadCustomPayload(options.payload);
371
420
 
372
421
  // Test each datasource
373
422
  for (const datasourceFile of datasourcesToTest) {
@@ -377,12 +426,8 @@ async function testExternalSystemIntegration(appName, options = {}) {
377
426
  logger.log(chalk.blue(`\nšŸ“” Testing datasource: ${datasourceKey}`));
378
427
 
379
428
  // Determine payload to use
380
- let payloadTemplate;
381
- if (customPayload) {
382
- payloadTemplate = customPayload;
383
- } else if (datasource.testPayload && datasource.testPayload.payloadTemplate) {
384
- payloadTemplate = datasource.testPayload.payloadTemplate;
385
- } else {
429
+ const payloadTemplate = testHelpers.determinePayloadTemplate(datasource, datasourceKey, customPayload);
430
+ if (!payloadTemplate) {
386
431
  logger.log(chalk.yellow(` ⚠ No test payload found for ${datasourceKey}, skipping...`));
387
432
  results.datasourceResults.push({
388
433
  key: datasourceKey,
@@ -393,23 +438,14 @@ async function testExternalSystemIntegration(appName, options = {}) {
393
438
  }
394
439
 
395
440
  try {
396
- const testResponse = await callPipelineTestEndpoint(
441
+ const datasourceResult = await testHelpers.testSingleDatasource({
397
442
  systemKey,
398
443
  datasourceKey,
399
444
  payloadTemplate,
400
445
  dataplaneUrl,
401
446
  authConfig,
402
- parseInt(options.timeout, 10) || 30000
403
- );
404
-
405
- const datasourceResult = {
406
- key: datasourceKey,
407
- skipped: false,
408
- success: testResponse.success !== false,
409
- validationResults: testResponse.validationResults || {},
410
- fieldMappingResults: testResponse.fieldMappingResults || {},
411
- endpointTestResults: testResponse.endpointTestResults || {}
412
- };
447
+ timeout: parseInt(options.timeout, 10) || 30000
448
+ });
413
449
 
414
450
  if (!datasourceResult.success) {
415
451
  results.success = false;
@@ -438,6 +474,5 @@ module.exports = {
438
474
  testExternalSystemIntegration,
439
475
  displayTestResults,
440
476
  displayIntegrationTestResults,
441
- callPipelineTestEndpoint,
442
477
  retryApiCall
443
478
  };