@aifabrix/builder 2.41.0 → 2.42.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 (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -19,18 +19,74 @@ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
19
19
 
20
20
  // Register Handlebars helper for equality check
21
21
  handlebars.registerHelper('eq', (a, b) => a === b);
22
+ handlebars.registerHelper('json', (obj) => JSON.stringify(obj));
22
23
 
23
24
  /**
24
- * Generates external system JSON file from template
25
+ * Build authentication object per schema authenticationVariablesByMethod.
26
+ * Security values use kv://<systemKey>/<key> pattern.
27
+ * @param {string} systemKey - External system key
28
+ * @param {string} authType - Auth method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none)
29
+ * @returns {{ method: string, variables: Object, security: Object }} Authentication object
30
+ */
31
+ function buildAuthenticationFromMethod(systemKey, authType) {
32
+ const kv = (key) => `kv://${systemKey}/${key}`;
33
+ const method = authType || 'apikey';
34
+ const base = 'https://api.example.com';
35
+
36
+ const authMap = {
37
+ oauth2: {
38
+ variables: { baseUrl: base, tokenUrl: `${base}/oauth/token`, authorizationUrl: `${base}/oauth/authorize` },
39
+ security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
40
+ },
41
+ aad: {
42
+ variables: { baseUrl: base, tokenUrl: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', tenantId: '{tenant-id}' },
43
+ security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
44
+ },
45
+ apikey: {
46
+ variables: { baseUrl: base, headerName: 'X-API-Key' },
47
+ security: { apiKey: kv('apikey') }
48
+ },
49
+ basic: {
50
+ variables: { baseUrl: base },
51
+ security: { username: kv('username'), password: kv('password') }
52
+ },
53
+ queryParam: {
54
+ variables: { baseUrl: base, paramName: 'api_key' },
55
+ security: { paramValue: kv('paramvalue') }
56
+ },
57
+ oidc: {
58
+ variables: { openIdConfigUrl: 'https://example.com/.well-known/openid-configuration', clientId: 'app-id' },
59
+ security: {}
60
+ },
61
+ hmac: {
62
+ variables: { baseUrl: base, algorithm: 'sha256', signatureHeader: 'X-Signature' },
63
+ security: { signingSecret: kv('signingsecret') }
64
+ },
65
+ none: {
66
+ variables: {},
67
+ security: {}
68
+ }
69
+ };
70
+
71
+ const auth = authMap[method] || authMap.apikey;
72
+ return { method, variables: auth.variables, security: auth.security };
73
+ }
74
+
75
+ /** Target extension for format */
76
+ const FORMAT_EXT = { yaml: '.yaml', json: '.json' };
77
+
78
+ /**
79
+ * Generates external system file from template
25
80
  * @async
26
81
  * @function generateExternalSystemTemplate
27
82
  * @param {string} appPath - Application directory path
28
83
  * @param {string} systemKey - System key
29
84
  * @param {Object} config - System configuration
85
+ * @param {string} [format] - Output format: 'yaml' (default) or 'json'
30
86
  * @returns {Promise<string>} Path to generated file
31
87
  * @throws {Error} If generation fails
32
88
  */
33
- async function generateExternalSystemTemplate(appPath, systemKey, config) {
89
+ async function generateExternalSystemTemplate(appPath, systemKey, config, format = 'yaml') {
34
90
  try {
35
91
  const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-system.json.hbs');
36
92
  const templateContent = await fs.readFile(templatePath, 'utf8');
@@ -41,13 +97,15 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
41
97
  groups: role.groups || role.Groups || undefined
42
98
  }));
43
99
 
100
+ const authType = config.authType || 'apikey';
101
+ const authentication = buildAuthenticationFromMethod(systemKey, authType);
102
+
44
103
  const context = {
45
104
  systemKey: systemKey,
46
105
  systemDisplayName: config.systemDisplayName || systemKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
47
106
  systemDescription: config.systemDescription || `External system integration for ${systemKey}`,
48
107
  systemType: config.systemType || 'openapi',
49
- authType: config.authType || 'apikey',
50
- baseUrl: config.baseUrl || null,
108
+ authentication,
51
109
  roles: roles || null,
52
110
  permissions: config.permissions || null
53
111
  };
@@ -55,10 +113,9 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
55
113
  const rendered = template(context);
56
114
  const parsed = JSON.parse(rendered);
57
115
 
58
- // Generate in same folder as application.yaml (new structure)
59
- // Use naming: <app-name>-system.yaml
60
- const outputPath = path.join(appPath, `${systemKey}-system.yaml`);
61
- writeConfigFile(outputPath, parsed);
116
+ const ext = FORMAT_EXT[format === 'json' ? 'json' : 'yaml'] || '.yaml';
117
+ const outputPath = path.join(appPath, `${systemKey}-system${ext}`);
118
+ writeConfigFile(outputPath, parsed, format === 'json' ? 'json' : 'yaml');
62
119
 
63
120
  return outputPath;
64
121
  } catch (error) {
@@ -66,51 +123,104 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
66
123
  }
67
124
  }
68
125
 
126
+ /** Schema-valid entityType values (external-datasource.schema.json) */
127
+ const SCHEMA_ENTITY_TYPES = ['recordStorage', 'documentStorage', 'vectorStore', 'messageService', 'none'];
128
+
129
+ /** Maps resourceType to schema entityType for generation */
130
+ function resourceTypeToSchemaEntityType(resourceType) {
131
+ return resourceType === 'document' ? 'documentStorage' : 'recordStorage';
132
+ }
133
+
134
+ /**
135
+ * Build datasource context object for template rendering
136
+ * @param {Object} opts - Options
137
+ * @param {Object} opts.config - Datasource configuration
138
+ * @param {string} opts.datasourceKey - Datasource key
139
+ * @param {Object} opts.dimensions - Dimensions map
140
+ * @param {Object} opts.attributes - Attributes map
141
+ * @param {string} opts.fullDatasourceKey - Full key including system
142
+ * @param {string} opts.entityKey - Entity key portion
143
+ * @param {string} opts.schemaEntityType - Schema entity type
144
+ * @param {string} opts.resourceType - Resource type
145
+ * @returns {Object} Handlebars context
146
+ */
147
+ function buildDatasourceContext({ config, datasourceKey, dimensions, attributes, fullDatasourceKey, entityKey, schemaEntityType, resourceType }) {
148
+ const displayName = config.datasourceDisplayName || datasourceKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
149
+ const description = config.datasourceDescription || `External datasource for ${datasourceKey}`;
150
+ const primaryKey = Array.isArray(config.primaryKey) && config.primaryKey.length > 0
151
+ ? config.primaryKey
152
+ : ['id'];
153
+ return {
154
+ fullDatasourceKey,
155
+ entityKey,
156
+ datasourceDisplayName: displayName,
157
+ datasourceDescription: description,
158
+ systemKey: config.systemKey,
159
+ schemaEntityType,
160
+ resourceType,
161
+ primaryKey,
162
+ systemType: config.systemType || 'openapi',
163
+ dimensions: Object.keys(dimensions).length > 0 ? dimensions : null,
164
+ attributes: Object.keys(attributes).length > 0 ? attributes : null,
165
+ raw: { id: '{{raw.id}}', name: '{{raw.name}}' }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Write datasource output in requested format
171
+ * @param {string} outputPath - Output file path
172
+ * @param {string} rendered - Rendered template content
173
+ * @param {string} format - 'yaml' or 'json'
174
+ * @returns {Promise<void>}
175
+ */
176
+ async function writeDatasourceOutput(outputPath, rendered, format) {
177
+ if (format === 'json') {
178
+ const yaml = require('js-yaml');
179
+ const parsed = yaml.load(rendered);
180
+ writeConfigFile(outputPath, parsed, 'json');
181
+ } else {
182
+ await fs.writeFile(outputPath, rendered, 'utf8');
183
+ }
184
+ }
185
+
69
186
  /**
70
- * Generates external datasource JSON file from template
187
+ * Generates external datasource file from template with entityType-driven optional commented sections
71
188
  * @async
72
189
  * @function generateExternalDataSourceTemplate
73
190
  * @param {string} appPath - Application directory path
74
- * @param {string} datasourceKey - Datasource key
191
+ * @param {string} datasourceKey - Datasource key (e.g. entity1, company)
75
192
  * @param {Object} config - Datasource configuration
193
+ * @param {string} [format] - Output format: 'yaml' (default) or 'json'
76
194
  * @returns {Promise<string>} Path to generated file
77
195
  * @throws {Error} If generation fails
78
196
  */
79
- async function generateExternalDataSourceTemplate(appPath, datasourceKey, config) {
197
+ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config, format = 'yaml') {
80
198
  try {
81
- const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-datasource.json.hbs');
199
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-datasource.yaml.hbs');
82
200
  const templateContent = await fs.readFile(templatePath, 'utf8');
83
201
  const template = handlebars.compile(templateContent);
84
202
 
85
203
  const dimensions = config.dimensions || {};
86
204
  const attributes = config.attributes || {};
87
- const context = {
88
- datasourceKey: datasourceKey,
89
- datasourceDisplayName: config.datasourceDisplayName || datasourceKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
90
- datasourceDescription: config.datasourceDescription || `External datasource for ${datasourceKey}`,
91
- systemKey: config.systemKey,
92
- entityType: config.entityType || datasourceKey.split('-').pop(),
93
- resourceType: config.resourceType || 'document',
94
- systemType: config.systemType || 'openapi',
95
- // Pass non-empty objects so template uses custom block; empty/null so template uses schema-valid defaults
96
- dimensions: Object.keys(dimensions).length > 0 ? dimensions : null,
97
- attributes: Object.keys(attributes).length > 0 ? attributes : null,
98
- // Literal expression strings for default attribute block (schema: pipe-based DSL {{raw.path}})
99
- raw: { id: '{{raw.id}}', name: '{{raw.name}}' }
100
- };
205
+ const resourceType = config.resourceType || 'document';
206
+ const schemaEntityType = SCHEMA_ENTITY_TYPES.includes(config.entityType)
207
+ ? config.entityType
208
+ : resourceTypeToSchemaEntityType(resourceType);
101
209
 
102
- const rendered = template(context);
103
- const datasourceConfig = JSON.parse(rendered);
104
-
105
- // Generate in same folder as application.yaml (new structure)
106
- // Use naming: <app-name>-datasource-<datasource-key>.yaml
107
- // Extract datasource key (remove system key prefix if present)
108
- const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${config.systemKey}-`)
210
+ const prefix = `${config.systemKey}-`;
211
+ const fullDatasourceKey = datasourceKey.startsWith(prefix) ? datasourceKey : `${prefix}${datasourceKey}`;
212
+ const entityKey = (datasourceKey.includes('-') && datasourceKey.startsWith(prefix))
109
213
  ? datasourceKey.substring(config.systemKey.length + 1)
110
214
  : datasourceKey;
111
- const outputPath = path.join(appPath, `${config.systemKey}-datasource-${datasourceKeyOnly}.yaml`);
112
- writeConfigFile(outputPath, datasourceConfig);
113
215
 
216
+ const context = buildDatasourceContext({
217
+ config, datasourceKey, dimensions, attributes, fullDatasourceKey, entityKey, schemaEntityType, resourceType
218
+ });
219
+ const rendered = template(context);
220
+ const ext = FORMAT_EXT[format === 'json' ? 'json' : 'yaml'] || '.yaml';
221
+ const outputPath = path.join(appPath, `${config.systemKey}-datasource-${entityKey}${ext}`);
222
+
223
+ await writeDatasourceOutput(outputPath, rendered, format);
114
224
  return outputPath;
115
225
  } catch (error) {
116
226
  throw new Error(`Failed to generate external datasource template: ${error.message}`);
@@ -124,47 +234,51 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
124
234
  * @param {string} appPath - Application directory path
125
235
  * @param {string} appName - Application name
126
236
  * @param {Object} config - Configuration with external system details
237
+ * @param {string} [format] - Output format: 'yaml' (default) or 'json'
127
238
  * @returns {Promise<Object>} Object with system and datasource file paths
128
239
  * @throws {Error} If generation fails
129
240
  */
130
- async function generateExternalSystemFiles(appPath, appName, config) {
241
+ async function generateExternalSystemFiles(appPath, appName, config, format = 'yaml') {
131
242
  try {
132
243
  const systemKey = config.systemKey || appName;
133
244
  const datasourceCount = config.datasourceCount || 1;
245
+ const fmt = (format === 'json' ? 'json' : 'yaml');
134
246
 
135
- // Generate external system JSON
136
- const systemPath = await generateExternalSystemTemplate(appPath, systemKey, config);
247
+ // Generate external system file
248
+ const systemPath = await generateExternalSystemTemplate(appPath, systemKey, config, fmt);
137
249
  logger.log(chalk.green(`✓ Generated external system: ${path.basename(systemPath)}`));
138
250
 
139
251
  // Generate datasource JSON files
140
252
  const datasourcePaths = [];
141
253
  const resourceTypes = ['customer', 'contact', 'person', 'document', 'deal'];
142
254
 
255
+ const schemaEntityType = SCHEMA_ENTITY_TYPES.includes(config.entityType)
256
+ ? config.entityType
257
+ : 'recordStorage';
258
+
143
259
  for (let i = 0; i < datasourceCount; i++) {
144
- const entityType = `entity${i + 1}`;
145
- // For datasource key, use just the entity type (will be prefixed with app-name-deploy-)
146
- const datasourceKey = entityType;
260
+ const entityKey = `entity${i + 1}`;
261
+ const datasourceKey = entityKey;
147
262
  const resourceType = resourceTypes[i % resourceTypes.length];
148
263
 
149
264
  const datasourceConfig = {
150
265
  systemKey: systemKey,
151
- entityType: entityType,
266
+ entityType: schemaEntityType,
152
267
  resourceType: resourceType,
153
268
  systemType: config.systemType || 'openapi',
154
- datasourceDisplayName: `${config.systemDisplayName || systemKey} ${entityType}`,
155
- datasourceDescription: `External datasource for ${entityType} entity`,
269
+ datasourceDisplayName: `${config.systemDisplayName || systemKey} ${entityKey}`,
270
+ datasourceDescription: `External datasource for ${entityKey} entity`,
156
271
  dimensions: config.dimensions || {},
157
272
  attributes: config.attributes || {}
158
273
  };
159
274
 
160
- // Generate with new naming: <app-name>-datasource-<entity-key>.json
161
- const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig);
275
+ const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig, fmt);
162
276
  datasourcePaths.push(datasourcePath);
163
277
  logger.log(chalk.green(`✓ Generated datasource: ${path.basename(datasourcePath)}`));
164
278
  }
165
279
 
166
- // Update application.yaml with externalIntegration block
167
- await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths);
280
+ // Update application config with externalIntegration block
281
+ await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths, fmt);
168
282
 
169
283
  return {
170
284
  systemPath,
@@ -176,36 +290,70 @@ async function generateExternalSystemFiles(appPath, appName, config) {
176
290
  }
177
291
 
178
292
  /**
179
- * Updates application.yaml with externalIntegration block
293
+ * Resolve application config path and load variables
294
+ * @param {string} appPath - Application directory path
295
+ * @param {string} ext - Config file extension
296
+ * @returns {{ configPath: string, variables: Object }}
297
+ */
298
+ function resolveConfigAndVariables(appPath, ext) {
299
+ try {
300
+ const configPath = resolveApplicationConfigPath(appPath);
301
+ const variables = loadConfigFile(configPath) || {};
302
+ return { configPath, variables };
303
+ } catch (resolveErr) {
304
+ const msg = (resolveErr && resolveErr.message) || '';
305
+ const isNotFound = msg.includes('Config file not found') || msg.includes('Application config not found');
306
+ if (!isNotFound) throw resolveErr;
307
+ return { configPath: path.join(appPath, `application${ext}`), variables: {} };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Remove old config file if it differs from target path
313
+ * @param {string} configPath - Current config path
314
+ * @param {string} targetPath - Target output path
315
+ */
316
+ function maybeRemoveOldConfig(configPath, targetPath) {
317
+ const same = configPath === targetPath || path.normalize(configPath) === path.normalize(targetPath);
318
+ if (same) return;
319
+ const fsSync = require('fs');
320
+ if (fsSync.existsSync(configPath)) fsSync.unlinkSync(configPath);
321
+ }
322
+
323
+ /**
324
+ * Updates application config with externalIntegration block
180
325
  * @async
181
326
  * @function updateVariablesYamlWithExternalIntegration
182
327
  * @param {string} appPath - Application directory path
183
328
  * @param {string} systemKey - System key
184
329
  * @param {Array<string>} datasourcePaths - Array of datasource file paths
330
+ * @param {string} [format] - Output format: 'yaml' (default) or 'json'
185
331
  * @throws {Error} If update fails
186
332
  */
187
- async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths) {
333
+ async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths, format = 'yaml') {
188
334
  try {
189
- const configPath = resolveApplicationConfigPath(appPath);
190
- const variables = loadConfigFile(configPath);
335
+ const fmt = format === 'json' ? 'json' : 'yaml';
336
+ const ext = FORMAT_EXT[fmt] || '.yaml';
337
+ const { configPath, variables } = resolveConfigAndVariables(appPath, ext);
191
338
 
192
- // Add externalIntegration block
193
- // Files are in same folder, so schemaBasePath is './'
194
339
  variables.externalIntegration = {
195
340
  schemaBasePath: './',
196
- systems: [`${systemKey}-system.yaml`],
341
+ systems: [`${systemKey}-system${ext}`],
197
342
  dataSources: datasourcePaths.map(p => path.basename(p)),
198
343
  autopublish: true,
199
344
  version: '1.0.0'
200
345
  };
201
346
 
202
- writeConfigFile(configPath, variables);
347
+ const targetPath = path.join(appPath, `application${ext}`);
348
+ writeConfigFile(targetPath, variables, fmt);
349
+ maybeRemoveOldConfig(configPath, targetPath);
203
350
  } catch (error) {
204
351
  throw new Error(`Failed to update application config: ${error.message}`);
205
352
  }
206
353
  }
207
354
 
208
355
  module.exports = {
356
+ buildAuthenticationFromMethod,
209
357
  generateExternalSystemTemplate,
210
358
  generateExternalDataSourceTemplate,
211
359
  generateExternalSystemFiles
@@ -32,7 +32,8 @@ async function executeDatasourceTest(systemKey, datasourceKey, payloadTemplate,
32
32
  payloadTemplate,
33
33
  dataplaneUrl,
34
34
  authConfig,
35
- timeout: parseInt(options.timeout, 10) || 30000
35
+ timeout: parseInt(options.timeout, 10) || 30000,
36
+ includeDebug: !!options.debug
36
37
  });
37
38
  return datasourceResult;
38
39
  } catch (error) {
@@ -0,0 +1,73 @@
1
+ /**
2
+ * System-level pipeline test - single API call for all datasources
3
+ * @fileoverview System-level test execution for integration tests
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+ /* eslint-disable max-statements,complexity -- Map response to datasource results */
8
+
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { testSystemViaPipeline } = require('../api/pipeline.api');
13
+ const { writeTestLog } = require('../utils/test-log-writer');
14
+ const { getIntegrationPath } = require('../utils/paths');
15
+
16
+ /**
17
+ * Run system-level pipeline test and map response to datasource results
18
+ * @async
19
+ * @param {Object} params - Parameters
20
+ * @param {string} params.appName - Application name
21
+ * @param {string} params.systemKey - System key
22
+ * @param {Object} params.authConfig - Auth config
23
+ * @param {string} params.dataplaneUrl - Dataplane URL
24
+ * @param {boolean} [params.debug] - Write debug log
25
+ * @param {number} [params.timeout] - Request timeout
26
+ * @returns {Promise<{success: boolean, datasourceResults: Object[]}>}
27
+ */
28
+ async function runSystemLevelTest({ appName, systemKey, authConfig, dataplaneUrl, debug, timeout }) {
29
+ const testData = { includeDebug: !!debug };
30
+ const response = await testSystemViaPipeline(dataplaneUrl, systemKey, authConfig, testData, { timeout });
31
+ const data = response.data || response;
32
+
33
+ if (debug) {
34
+ const appPath = getIntegrationPath(appName);
35
+ const integrationDir = path.dirname(appPath);
36
+ const logPath = await writeTestLog(appName, { request: { systemKey, includeDebug: true }, response: data }, 'test-integration', integrationDir);
37
+ logger.log(chalk.gray(` Debug log: ${logPath}`));
38
+ }
39
+
40
+ const rawResults = data.datasourceResults || data.results || data.data?.datasourceResults || (Array.isArray(data) ? data : []);
41
+ const datasourceResults = [];
42
+ let success = true;
43
+
44
+ for (const r of rawResults) {
45
+ const dsKey = r.key || r.datasourceKey;
46
+ const dsResult = {
47
+ key: dsKey,
48
+ success: r.success !== false,
49
+ skipped: !!r.skipped,
50
+ reason: r.reason,
51
+ validationResults: r.validationResults || {},
52
+ fieldMappingResults: r.fieldMappingResults || {},
53
+ endpointTestResults: r.endpointTestResults || {}
54
+ };
55
+ if (r.error) dsResult.error = r.error;
56
+ if (!dsResult.success && !dsResult.skipped) success = false;
57
+ datasourceResults.push(dsResult);
58
+ }
59
+
60
+ if (rawResults.length === 0 && data.success === false) {
61
+ success = false;
62
+ datasourceResults.push({
63
+ key: 'system',
64
+ success: false,
65
+ skipped: false,
66
+ error: data.error || data.formattedError || 'System test failed'
67
+ });
68
+ }
69
+
70
+ return { success, datasourceResults };
71
+ }
72
+
73
+ module.exports = { runSystemLevelTest };
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * External System Testing Module
3
- *
4
- * Provides unit testing (local validation) and integration testing (via dataplane)
5
- * for external systems and datasources.
6
- *
3
+ * Provides unit testing (local validation) and integration testing (via dataplane).
7
4
  * @fileoverview External system testing functionality for AI Fabrix Builder
8
5
  * @author AI Fabrix Team
9
6
  * @version 2.0.0
10
7
  */
8
+ /* eslint-disable max-lines,max-lines-per-function,max-statements,complexity,max-depth -- Integration test flow with system-level and per-datasource paths */
11
9
 
12
10
  const fs = require('fs').promises;
13
11
  const fsSync = require('fs');
@@ -36,6 +34,7 @@ const {
36
34
  const {
37
35
  testSingleDatasourceIntegration
38
36
  } = require('./test-execution');
37
+ const { runSystemLevelTest } = require('./test-system-level');
39
38
 
40
39
  /**
41
40
  * Loads and parses application config file
@@ -426,22 +425,56 @@ async function testExternalSystemIntegration(appName, options = {}) {
426
425
  datasourceResults: []
427
426
  };
428
427
 
429
- // Test each datasource
430
- for (const datasourceFile of datasourcesToTest) {
431
- const datasourceResult = await testSingleDatasourceIntegration(
432
- datasourceFile,
433
- systemKey,
434
- dataplaneUrl,
435
- authConfig,
436
- customPayload,
437
- options
438
- );
439
-
440
- if (!datasourceResult.success && !datasourceResult.skipped) {
441
- results.success = false;
428
+ const useSystemLevel = !options.datasource && datasourcesToTest.length > 0 && !customPayload;
429
+ const timeout = parseInt(options.timeout, 10) || 30000;
430
+
431
+ if (useSystemLevel && datasourcesToTest.length > 1) {
432
+ try {
433
+ const systemResult = await runSystemLevelTest({
434
+ appName, systemKey, authConfig, dataplaneUrl, debug: options.debug, timeout
435
+ });
436
+ results.success = systemResult.success;
437
+ results.datasourceResults = systemResult.datasourceResults;
438
+ } catch (err) {
439
+ if (options.debug) {
440
+ try {
441
+ const { writeTestLog } = require('../utils/test-log-writer');
442
+ const { getIntegrationPath } = require('../utils/paths');
443
+ const appPath = getIntegrationPath(appName);
444
+ await writeTestLog(appName, { request: { systemKey }, error: err.message }, 'test-integration', path.dirname(appPath));
445
+ } catch (_) { /* ignore */ }
446
+ }
447
+ throw err;
448
+ }
449
+ } else {
450
+ for (const datasourceFile of datasourcesToTest) {
451
+ const datasourceResult = await testSingleDatasourceIntegration(
452
+ datasourceFile,
453
+ systemKey,
454
+ dataplaneUrl,
455
+ authConfig,
456
+ customPayload,
457
+ options
458
+ );
459
+
460
+ if (!datasourceResult.success && !datasourceResult.skipped) {
461
+ results.success = false;
462
+ }
463
+
464
+ results.datasourceResults.push(datasourceResult);
442
465
  }
443
466
 
444
- results.datasourceResults.push(datasourceResult);
467
+ if (options.debug && results.datasourceResults.length > 0) {
468
+ try {
469
+ const { writeTestLog } = require('../utils/test-log-writer');
470
+ const { getIntegrationPath } = require('../utils/paths');
471
+ const appPath = getIntegrationPath(appName);
472
+ await writeTestLog(appName, {
473
+ request: { systemKey, datasource: options.datasource, includeDebug: true },
474
+ response: results
475
+ }, 'test-integration', path.dirname(appPath));
476
+ } catch (_) { /* ignore */ }
477
+ }
445
478
  }
446
479
 
447
480
  return results;
@@ -92,6 +92,7 @@ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
92
92
  * @param {string} appName - Application name
93
93
  * @param {Object} [options] - Optional parameters
94
94
  * @param {string} [options.appPath] - Application path (if provided, skips detection)
95
+ * @param {boolean} [options.skipMissingDatasourceFiles] - If true, skip datasource files that don't exist (e.g. when generating deploy JSON)
95
96
  * @returns {Promise<Object>} Controller manifest object
96
97
  * @throws {Error} If generation fails
97
98
  *
@@ -125,7 +126,9 @@ async function generateControllerManifest(appName, options = {}) {
125
126
  }
126
127
  const [systemJson, datasourceJsons] = await Promise.all([
127
128
  loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]),
128
- loadDatasourceFiles(appPath, schemaBasePath, variables.externalIntegration.dataSources || [])
129
+ loadDatasourceFiles(appPath, schemaBasePath, variables.externalIntegration.dataSources || [], {
130
+ skipMissingDatasourceFiles: options.skipMissingDatasourceFiles
131
+ })
129
132
  ]);
130
133
  const appVersion = variables.app?.version || variables.externalIntegration?.version || '1.0.0';
131
134
  const externalIntegration = {
@@ -150,6 +153,30 @@ async function generateControllerManifest(appName, options = {}) {
150
153
  };
151
154
  }
152
155
 
156
+ /**
157
+ * Returns deploy-file shape: system + dataSources only (no file-name lists).
158
+ * Use when writing *-deploy.json to disk; file names live in application config only.
159
+ *
160
+ * @function toDeployJsonShape
161
+ * @param {Object} manifest - Full controller manifest from generateControllerManifest
162
+ * @returns {Object} { key, displayName, description, type, version, system, dataSources }
163
+ */
164
+ function toDeployJsonShape(manifest) {
165
+ if (!manifest || typeof manifest !== 'object') {
166
+ throw new Error('Manifest is required');
167
+ }
168
+ return {
169
+ key: manifest.key,
170
+ displayName: manifest.displayName,
171
+ description: manifest.description,
172
+ type: manifest.type || 'external',
173
+ version: manifest.version || '1.0.0',
174
+ system: manifest.system,
175
+ dataSources: Array.isArray(manifest.dataSources) ? manifest.dataSources : []
176
+ };
177
+ }
178
+
153
179
  module.exports = {
154
- generateControllerManifest
180
+ generateControllerManifest,
181
+ toDeployJsonShape
155
182
  };
@@ -192,7 +192,7 @@ async function writeSplitExternalSchemaFiles({ outputDir, systemKey, application
192
192
  const rbac = extractRbacYaml(application);
193
193
  let rbacPath = null;
194
194
  if (rbac) {
195
- rbacPath = path.join(outputDir, 'rbac.yml');
195
+ rbacPath = path.join(outputDir, 'rbac.yaml');
196
196
  await writeYamlFile(rbacPath, rbac, { indent: 2, lineWidth: -1 });
197
197
  }
198
198
 
@@ -174,11 +174,14 @@ async function loadSystemFile(appPath, schemaBasePath, systemFileName) {
174
174
  * @param {string} appPath - Application path
175
175
  * @param {string} schemaBasePath - Schema base path
176
176
  * @param {Array<string>} datasourceFiles - Array of datasource file names
177
+ * @param {Object} [options] - Options
178
+ * @param {boolean} [options.skipMissingDatasourceFiles] - If true, skip missing files (e.g. when generating deploy JSON) instead of throwing
177
179
  * @returns {Promise<Array<Object>>} Array of datasource JSON objects
178
- * @throws {Error} If files cannot be loaded
180
+ * @throws {Error} If a file is missing and skipMissingDatasourceFiles is not set
179
181
  */
180
- async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles) {
182
+ async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles, options = {}) {
181
183
  const datasourceJsons = [];
184
+ const { skipMissingDatasourceFiles } = options;
182
185
 
183
186
  for (const datasourceFile of datasourceFiles) {
184
187
  const datasourcePath = path.isAbsolute(schemaBasePath)
@@ -186,6 +189,9 @@ async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles) {
186
189
  : path.join(appPath, schemaBasePath, datasourceFile);
187
190
 
188
191
  if (!fs.existsSync(datasourcePath)) {
192
+ if (skipMissingDatasourceFiles) {
193
+ continue;
194
+ }
189
195
  throw new Error(`Datasource file not found: ${datasourcePath}`);
190
196
  }
191
197
 
@@ -417,6 +423,7 @@ module.exports = {
417
423
  generateExternalSystemApplicationSchema,
418
424
  splitExternalApplicationSchema,
419
425
  loadSystemFile,
420
- loadDatasourceFiles
426
+ loadDatasourceFiles,
427
+ loadExternalIntegrationConfig
421
428
  };
422
429
 
@@ -249,7 +249,10 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
249
249
  * @returns {Promise<string>} Path to written deploy JSON
250
250
  */
251
251
  async function writeExternalDeployJson(appName, appPath, options) {
252
- const manifest = await generateControllerManifest(appName, options);
252
+ const manifest = await generateControllerManifest(appName, {
253
+ ...options,
254
+ skipMissingDatasourceFiles: true
255
+ });
253
256
  let effectivePort = 3000;
254
257
  try {
255
258
  const variablesPath = resolveApplicationConfigPath(appPath);