@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
@@ -0,0 +1,157 @@
1
+ /**
2
+ * env.template authentication kv:// validation helpers
3
+ *
4
+ * Collects required kv paths from system configs and extracts kv paths from env.template
5
+ * for validating that external integrations have all authentication secrets in env.template.
6
+ *
7
+ * @fileoverview Auth kv validation for env.template
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { loadExternalIntegrationConfig, loadSystemFile } = require('../generator/external');
13
+
14
+ /**
15
+ * Extracts all kv:// paths from env.template content (RHS of VAR=value lines).
16
+ * Uses same regex as validateKvReferencesInLines.
17
+ *
18
+ * @function extractKvPathsFromEnvTemplate
19
+ * @param {string} content - env.template file content
20
+ * @returns {Set<string>} Set of kv:// paths found (e.g. kv://hubspot/client-id)
21
+ *
22
+ * @example
23
+ * const paths = extractKvPathsFromEnvTemplate('CLIENT_ID=kv://hubspot/client-id\nPORT=3000');
24
+ * // paths has 'kv://hubspot/client-id'
25
+ */
26
+ function extractKvPathsFromEnvTemplate(content) {
27
+ const paths = new Set();
28
+ const lines = content.split('\n');
29
+ for (const line of lines) {
30
+ const trimmed = line.trim();
31
+ if (!trimmed || trimmed.startsWith('#')) continue;
32
+ if (!trimmed.includes('=')) continue;
33
+ const [_key, value] = trimmed.split('=', 2);
34
+ const val = (value || '').trim();
35
+ const matches = val.match(/kv:\/\/[^\s]*/g) || [];
36
+ for (const fullRef of matches) {
37
+ paths.add(fullRef);
38
+ }
39
+ }
40
+ return paths;
41
+ }
42
+
43
+ /**
44
+ * Extracts kv:// paths from commented lines in env.template (e.g. # KEY=kv://path or # kv://path).
45
+ * Used to treat commented-out keys as intentionally disabled for auth-coverage validation.
46
+ * Scans the whole line after '#' so both key=value and bare/commented refs are recognized.
47
+ *
48
+ * @function extractKvPathsFromCommentedLines
49
+ * @param {string} content - env.template file content
50
+ * @returns {Set<string>} Set of kv:// paths found in commented lines (e.g. kv://avoma/apikey)
51
+ */
52
+ function extractKvPathsFromCommentedLines(content) {
53
+ const paths = new Set();
54
+ const lines = content.split('\n');
55
+ for (const line of lines) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed.startsWith('#')) continue;
58
+ const afterHash = trimmed.slice(1).trim();
59
+ const matches = afterHash.match(/kv:\/\/[^\s]*/g) || [];
60
+ for (const fullRef of matches) {
61
+ paths.add(fullRef);
62
+ }
63
+ }
64
+ return paths;
65
+ }
66
+
67
+ /**
68
+ * Returns true if the required path is present in the set or matches any entry case-insensitively.
69
+ * Used so commented-out keys match required paths regardless of kv path casing (e.g. apiKey vs apikey).
70
+ *
71
+ * @function setHasPathIgnoreCase
72
+ * @param {Set<string>} pathSet - Set of kv paths (e.g. from commented lines)
73
+ * @param {string} requiredPath - Required path from authentication.security
74
+ * @returns {boolean} True if requiredPath is in pathSet or matches any element when lowercased
75
+ */
76
+ function setHasPathIgnoreCase(pathSet, requiredPath) {
77
+ if (pathSet.has(requiredPath)) return true;
78
+ const requiredLower = requiredPath.toLowerCase();
79
+ for (const p of pathSet) {
80
+ if (p.toLowerCase() === requiredLower) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Collects required kv:// paths from authentication.security of all system configs.
87
+ * For external integrations only. On config load failure, returns empty set and optional warning.
88
+ *
89
+ * @async
90
+ * @function collectRequiredAuthKvPaths
91
+ * @param {string} appPath - Application directory path
92
+ * @param {Object} [options] - Options (reserved)
93
+ * @returns {Promise<{ requiredPaths: Set<string>, warning?: string }>} Required kv paths and optional warning
94
+ *
95
+ * @example
96
+ * const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot');
97
+ * // requiredPaths has kv:// paths from authentication.security
98
+ */
99
+ async function collectRequiredAuthKvPaths(appPath, _options = {}) {
100
+ const requiredPaths = new Set();
101
+ try {
102
+ const { schemaBasePath, systemFiles } = await loadExternalIntegrationConfig(appPath);
103
+ for (const systemFileName of systemFiles) {
104
+ const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFileName);
105
+ const security = systemJson.authentication?.security;
106
+ if (!security || typeof security !== 'object') continue;
107
+ for (const val of Object.values(security)) {
108
+ if (typeof val === 'string' && /^kv:\/\/.+/.test(val)) {
109
+ requiredPaths.add(val);
110
+ }
111
+ }
112
+ }
113
+ return { requiredPaths };
114
+ } catch (error) {
115
+ return {
116
+ requiredPaths: new Set(),
117
+ warning: `Could not validate auth kv coverage (skip auth check): ${error.message}`
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Validates that env.template covers all authentication.security kv paths for external apps.
124
+ * Modifies errors and warnings in place.
125
+ *
126
+ * @async
127
+ * @function validateAuthKvCoverage
128
+ * @param {string} appPath - Application path
129
+ * @param {string} content - env.template content
130
+ * @param {string[]} errors - Errors array to push to
131
+ * @param {string[]} warnings - Warnings array to push to
132
+ * @param {Object} [options] - Options
133
+ */
134
+ async function validateAuthKvCoverage(appPath, content, errors, warnings, options = {}) {
135
+ const authResult = await collectRequiredAuthKvPaths(appPath, options);
136
+ if (authResult.warning) warnings.push(authResult.warning);
137
+ if (authResult.requiredPaths.size === 0) return;
138
+ const actualPaths = extractKvPathsFromEnvTemplate(content);
139
+ const commentedPaths = extractKvPathsFromCommentedLines(content);
140
+ for (const requiredPath of authResult.requiredPaths) {
141
+ const inActive = actualPaths.has(requiredPath);
142
+ const inCommented = setHasPathIgnoreCase(commentedPaths, requiredPath);
143
+ if (!inActive && !inCommented) {
144
+ errors.push(
145
+ `env.template: Missing required authentication secret (required by authentication.security): add a variable with value ${requiredPath}`
146
+ );
147
+ }
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ extractKvPathsFromEnvTemplate,
153
+ extractKvPathsFromCommentedLines,
154
+ setHasPathIgnoreCase,
155
+ collectRequiredAuthKvPaths,
156
+ validateAuthKvCoverage
157
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * env.template kv:// reference validation (syntax only).
3
+ * Skips comment and empty lines. Used by validator.validateEnvTemplate.
4
+ *
5
+ * @fileoverview Kv reference validation for env.template lines
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ /**
11
+ * Validates kv:// references in env.template lines; pushes errors into the given array.
12
+ * Skips empty and comment (#) lines.
13
+ *
14
+ * @function validateKvReferencesInLines
15
+ * @param {string[]} lines - Lines of env.template content
16
+ * @param {string[]} errors - Array to push error messages into
17
+ */
18
+ function validateKvReferencesInLines(lines, errors) {
19
+ lines.forEach((line, index) => {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || trimmed.startsWith('#')) {
22
+ return;
23
+ }
24
+ const matches = line.match(/kv:\/\/[^\s]*/g) || [];
25
+ for (const fullRef of matches) {
26
+ const pathMatch = fullRef.match(/^kv:\/\/(.*)$/);
27
+ const pathStr = pathMatch ? pathMatch[1] : '';
28
+ const invalid = !pathStr || pathStr.startsWith('/') || pathStr.endsWith('/');
29
+ if (invalid) {
30
+ const hint = !pathStr
31
+ ? 'path is empty (use kv://secret-key)'
32
+ : pathStr.startsWith('/')
33
+ ? 'path must not start with / (use kv://secret-key not kv:///secret-key)'
34
+ : 'path must not end with / (use kv://secret-key not kv://secret-key/)';
35
+ errors.push(`env.template line ${index + 1}: Invalid kv:// reference "${fullRef}" - ${hint}`);
36
+ }
37
+ }
38
+ });
39
+ }
40
+
41
+ module.exports = { validateKvReferencesInLines };
@@ -156,6 +156,30 @@ function validateRequiredFields(manifest, errors) {
156
156
  });
157
157
  }
158
158
 
159
+ /**
160
+ * Validates that each datasource's systemKey matches the application system key.
161
+ * Matches dataplane upload API wording so integrators recognize the error.
162
+ *
163
+ * @function validateDatasourceSystemKeyAlignment
164
+ * @param {Object} manifest - Manifest object with system and dataSources
165
+ * @param {Array} errors - Errors array to append to
166
+ * @returns {void}
167
+ */
168
+ function validateDatasourceSystemKeyAlignment(manifest, errors) {
169
+ const systemKey = manifest.system?.key;
170
+ if (!manifest.dataSources || !Array.isArray(manifest.dataSources) || systemKey === null || systemKey === undefined || systemKey === '') {
171
+ return;
172
+ }
173
+ manifest.dataSources.forEach((datasource) => {
174
+ if (datasource.systemKey !== systemKey) {
175
+ const dsKey = datasource.key || 'unknown';
176
+ errors.push(
177
+ `Data source '${dsKey}' systemKey does not match application system key (expected '${systemKey}', got '${datasource.systemKey}')`
178
+ );
179
+ }
180
+ });
181
+ }
182
+
159
183
  /**
160
184
  * Validates controller deployment manifest for external systems
161
185
  * Validates manifest structure and inline system/dataSources against their schemas
@@ -187,6 +211,7 @@ async function validateControllerManifest(manifest) {
187
211
  validateManifestStructure(manifest, ajv, applicationSchema, errors);
188
212
  validateInlineSystem(manifest, ajv, externalSystemSchema, errors);
189
213
  validateDatasources(manifest, ajv, externalDatasourceSchema, errors, warnings);
214
+ validateDatasourceSystemKeyAlignment(manifest, errors);
190
215
  validateConditionalRequirements(manifest, errors, warnings);
191
216
  validateRequiredFields(manifest, errors);
192
217
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * External system authentication rules (OAuth2/AAD grantType, authorizationUrl, configuration).
3
+ * Used after schema validation for external system files.
4
+ *
5
+ * @fileoverview OAuth2/AAD and configuration rules for external system configs
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const VALID_GRANT_TYPES = ['client_credentials', 'authorization_code'];
11
+
12
+ /** Standard auth variable names (credential parameters supplied at runtime). Not allowed in configuration except BASEURL when auth is none. */
13
+ const STANDARD_AUTH_VAR_NAMES = new Set([
14
+ 'baseurl', 'clientid', 'clientsecret', 'tokenurl', 'apikey', 'username', 'password'
15
+ ]);
16
+
17
+ function trimVar(value) {
18
+ return (value !== undefined && value !== null ? String(value).trim() : '');
19
+ }
20
+
21
+ function isOAuth2OrAad(method) {
22
+ const m = (method && String(method).toLowerCase()) || '';
23
+ return m === 'oauth2' || m === 'aad';
24
+ }
25
+
26
+ /**
27
+ * Validates OAuth2/AAD grantType and conditional authorizationUrl for external system files.
28
+ * When method is oauth2 or aad: grantType (if present) must be client_credentials or authorization_code;
29
+ * when effective grant is authorization_code (explicit or default), authorizationUrl is required.
30
+ *
31
+ * @function validateOAuth2GrantTypeAndAuthorizationUrl
32
+ * @param {Object} parsed - Parsed external system object (must have authentication.variables when method is oauth2/aad)
33
+ * @param {string[]} errors - Array to push validation error messages into
34
+ */
35
+ function validateOAuth2GrantTypeAndAuthorizationUrl(parsed, errors) {
36
+ const auth = parsed?.authentication;
37
+ const variables = auth?.variables;
38
+ if (!variables || typeof variables !== 'object' || !isOAuth2OrAad(auth.method)) {
39
+ return;
40
+ }
41
+
42
+ const grantType = trimVar(variables.grantType);
43
+ if (grantType !== '' && !VALID_GRANT_TYPES.includes(grantType)) {
44
+ errors.push('authentication.variables.grantType must be one of: client_credentials, authorization_code');
45
+ return;
46
+ }
47
+
48
+ const effectiveGrant = grantType || 'authorization_code';
49
+ if (effectiveGrant === 'authorization_code' && trimVar(variables.authorizationUrl) === '') {
50
+ errors.push('authentication.variables.authorizationUrl is required when grantType is authorization_code or omitted');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Validates that the external system configuration array does not contain standard auth variable names.
56
+ * BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD are credential parameters
57
+ * supplied from the selected credential at runtime and must not be in configuration. Exception:
58
+ * BASEURL is only allowed in configuration when authentication.method is 'none'.
59
+ *
60
+ * @param {Object} parsed - Parsed external system object
61
+ * @param {string[]} errors - Array to push validation error messages into
62
+ */
63
+ function validateConfigurationNoStandardAuthVariables(parsed, errors) {
64
+ const config = parsed?.configuration;
65
+ if (!Array.isArray(config) || config.length === 0) return;
66
+ const method = (parsed?.authentication?.method && String(parsed.authentication.method).toLowerCase()) || '';
67
+ const authNone = method === 'none';
68
+ const allowedWhenNone = new Set(['baseurl']);
69
+ for (const item of config) {
70
+ const name = (item?.name && String(item.name).trim()) || '';
71
+ if (!name) continue;
72
+ const nameLower = name.toLowerCase();
73
+ if (!STANDARD_AUTH_VAR_NAMES.has(nameLower)) continue;
74
+ if (authNone && allowedWhenNone.has(nameLower)) continue;
75
+ errors.push(
76
+ `configuration must not contain standard auth variable '${name}'. ` +
77
+ 'Standard auth variables (BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD) are supplied from the selected credential at runtime. ' +
78
+ 'BASEURL is only allowed in configuration when authentication.method is \'none\'.'
79
+ );
80
+ }
81
+ }
82
+
83
+ module.exports = {
84
+ validateOAuth2GrantTypeAndAuthorizationUrl,
85
+ validateConfigurationNoStandardAuthVariables
86
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Batch validation helpers for validate --integration / --builder.
3
+ * @fileoverview Builds batch results and runs validation across multiple apps
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const { listIntegrationAppNames, listBuilderAppNames } = require('../utils/paths');
9
+
10
+ /**
11
+ * Collects error strings from a single validation result (various shapes).
12
+ * @param {Object} result - Single-app validation result
13
+ * @returns {string[]} Error messages
14
+ */
15
+ function collectResultErrors(result) {
16
+ const errs = [];
17
+ if (result.errors && Array.isArray(result.errors)) {
18
+ errs.push(...result.errors);
19
+ }
20
+ if (result.application && result.application.errors && Array.isArray(result.application.errors)) {
21
+ errs.push(...result.application.errors);
22
+ }
23
+ if (result.steps) {
24
+ ['application', 'components', 'manifest'].forEach(step => {
25
+ const s = result.steps[step];
26
+ if (s && s.errors && Array.isArray(s.errors)) {
27
+ errs.push(...s.errors);
28
+ }
29
+ });
30
+ }
31
+ return errs;
32
+ }
33
+
34
+ /**
35
+ * Collects warning strings from a single validation result.
36
+ * @param {Object} result - Single-app validation result
37
+ * @returns {string[]} Warning messages
38
+ */
39
+ function collectResultWarnings(result) {
40
+ const w = [];
41
+ if (result.warnings && Array.isArray(result.warnings)) {
42
+ w.push(...result.warnings);
43
+ }
44
+ if (result.application && result.application.warnings && Array.isArray(result.application.warnings)) {
45
+ w.push(...result.application.warnings);
46
+ }
47
+ if (result.steps) {
48
+ ['application', 'components', 'manifest'].forEach(step => {
49
+ const s = result.steps[step];
50
+ if (s && s.warnings && Array.isArray(s.warnings)) {
51
+ w.push(...s.warnings);
52
+ }
53
+ });
54
+ }
55
+ return w;
56
+ }
57
+
58
+ /**
59
+ * Builds batch result from per-app results. Each item has appName and either result or error.
60
+ * @param {Array<{appName: string, result?: Object, error?: string}>} items - Per-app results
61
+ * @returns {{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }}
62
+ */
63
+ function buildBatchResult(items) {
64
+ const errors = [];
65
+ const warnings = [];
66
+ items.forEach(item => {
67
+ if (item.error) {
68
+ errors.push(`${item.appName}: ${item.error}`);
69
+ } else if (item.result) {
70
+ collectResultErrors(item.result).forEach(e => errors.push(`${item.appName}: ${e}`));
71
+ collectResultWarnings(item.result).forEach(w => warnings.push(`${item.appName}: ${w}`));
72
+ }
73
+ });
74
+ const valid = items.every(item => item.result && item.result.valid);
75
+ return {
76
+ batch: true,
77
+ valid,
78
+ results: items,
79
+ errors,
80
+ warnings
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Validates all apps under integration/ (each as external system).
86
+ * @async
87
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
88
+ * @param {Object} [options] - Validation options
89
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
90
+ */
91
+ async function validateAllIntegrations(validateAppOrFile, options = {}) {
92
+ const names = listIntegrationAppNames();
93
+ const items = [];
94
+ for (const appName of names) {
95
+ try {
96
+ const result = await validateAppOrFile(appName, options);
97
+ items.push({ appName, result });
98
+ } catch (error) {
99
+ items.push({ appName, error: error.message || String(error) });
100
+ }
101
+ }
102
+ return buildBatchResult(items);
103
+ }
104
+
105
+ /**
106
+ * Validates all apps under builder/.
107
+ * @async
108
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
109
+ * @param {Object} [options] - Validation options
110
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
111
+ */
112
+ async function validateAllBuilderApps(validateAppOrFile, options = {}) {
113
+ const names = listBuilderAppNames();
114
+ const items = [];
115
+ for (const appName of names) {
116
+ try {
117
+ const result = await validateAppOrFile(appName, options);
118
+ items.push({ appName, result });
119
+ } catch (error) {
120
+ items.push({ appName, error: error.message || String(error) });
121
+ }
122
+ }
123
+ return buildBatchResult(items);
124
+ }
125
+
126
+ /**
127
+ * Validates all integration and builder apps in one run.
128
+ * @async
129
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
130
+ * @param {Object} [options] - Validation options
131
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
132
+ */
133
+ async function validateAll(validateAppOrFile, options = {}) {
134
+ const [integrationResult, builderResult] = await Promise.all([
135
+ validateAllIntegrations(validateAppOrFile, options),
136
+ validateAllBuilderApps(validateAppOrFile, options)
137
+ ]);
138
+ const mergedResults = [...integrationResult.results, ...builderResult.results];
139
+ return buildBatchResult(mergedResults);
140
+ }
141
+
142
+ module.exports = {
143
+ buildBatchResult,
144
+ collectResultErrors,
145
+ collectResultWarnings,
146
+ validateAllIntegrations,
147
+ validateAllBuilderApps,
148
+ validateAll
149
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Async validation of datasourceKeys against dataplane API
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { getPlatformDetails } = require('../api/wizard.api');
8
+ const { validateDatasourceKeysForPlatform } = require('./wizard-datasource-validation');
9
+
10
+ /**
11
+ * Validate datasourceKeys against platform's available datasources; throws if invalid
12
+ * @async
13
+ * @param {string} dataplaneUrl - Dataplane URL
14
+ * @param {Object} authConfig - Auth config
15
+ * @param {string} platformKey - Platform key
16
+ * @param {string[]} datasourceKeys - Datasource keys to validate
17
+ * @throws {Error} If any key is invalid
18
+ */
19
+ async function validateDatasourceKeysBeforePlatformConfig(dataplaneUrl, authConfig, platformKey, datasourceKeys) {
20
+ if (!Array.isArray(datasourceKeys) || datasourceKeys.length === 0) return;
21
+ const platformDetails = await getPlatformDetails(dataplaneUrl, authConfig, platformKey);
22
+ const datasources = platformDetails?.data?.datasources ?? platformDetails?.datasources ?? [];
23
+ const { valid, invalidKeys } = validateDatasourceKeysForPlatform(datasourceKeys, datasources);
24
+ if (!valid) {
25
+ const availableKeys = datasources.map(d => d.key).filter(Boolean);
26
+ throw new Error(
27
+ `Invalid datasource keys: [${invalidKeys.join(', ')}]. ` +
28
+ `Available for platform '${platformKey}': [${availableKeys.join(', ')}].`
29
+ );
30
+ }
31
+ }
32
+
33
+ module.exports = { validateDatasourceKeysBeforePlatformConfig };
@@ -8,6 +8,7 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const path = require('path');
11
12
  const chalk = require('chalk');
12
13
  const logger = require('../utils/logger');
13
14
  const { loadConfigFile } = require('../utils/config-format');
@@ -242,22 +243,25 @@ function displayApplicationStep(application) {
242
243
  * @returns {void}
243
244
  */
244
245
  function displayComponentsStep(components) {
245
- if (!components.files || components.files.length === 0) {
246
+ const hasFiles = components.files && components.files.length > 0;
247
+ const hasErrors = components.errors && components.errors.length > 0;
248
+ if (!hasFiles && !hasErrors) {
246
249
  return;
247
250
  }
248
251
 
249
252
  logger.log(chalk.blue('\nExternal Integration Files:'));
250
- components.files.forEach(file => {
251
- if (file.valid) {
252
- logger.log(chalk.green(` ✓ ${file.file} (${file.type})`));
253
- } else {
254
- logger.log(chalk.red(` ✗ ${file.file} (${file.type})`));
255
- }
256
- });
257
-
258
- if (components.errors && components.errors.length > 0) {
253
+ if (hasFiles) {
254
+ components.files.forEach(file => {
255
+ if (file.valid) {
256
+ logger.log(chalk.green(` ✓ ${file.file} (${file.type})`));
257
+ } else {
258
+ logger.log(chalk.red(` ✗ ${file.file} (${file.type})`));
259
+ }
260
+ });
261
+ }
262
+ if (hasErrors) {
259
263
  components.errors.forEach(error => {
260
- logger.log(chalk.red(` • ${error}`));
264
+ logger.log(chalk.red(` • ${error}`));
261
265
  });
262
266
  }
263
267
  }
@@ -301,7 +305,9 @@ function displayDimensionsStep(datasourceFiles) {
301
305
  */
302
306
  function displayManifestStep(manifest, componentFiles) {
303
307
  logger.log(chalk.blue('\nDeployment Manifest:'));
304
- if (manifest.valid) {
308
+ if (manifest.skipped) {
309
+ logger.log(chalk.yellow(' ⊘ Skipped (fix errors above first)'));
310
+ } else if (manifest.valid) {
305
311
  logger.log(chalk.green(' ✓ Full deployment manifest is valid'));
306
312
  if (componentFiles) {
307
313
  const datasourceFiles = componentFiles.filter(f => f.type === 'datasource' || f.type === 'external-datasource');
@@ -311,10 +317,14 @@ function displayManifestStep(manifest, componentFiles) {
311
317
  }
312
318
  } else {
313
319
  logger.log(chalk.red(' ✗ Full deployment manifest validation failed:'));
314
- if (manifest.errors && manifest.errors.length > 0) {
315
- manifest.errors.forEach(error => {
316
- logger.log(chalk.red(` • ${error}`));
320
+ const errs = manifest.errors && manifest.errors.length > 0 ? manifest.errors : [];
321
+ if (errs.length > 0) {
322
+ errs.forEach(error => {
323
+ const msg = typeof error === 'string' ? error : String(error);
324
+ logger.log(chalk.red(` • ${msg}`));
317
325
  });
326
+ } else {
327
+ logger.log(chalk.red(' • No error details available (check schema and manifest structure).'));
318
328
  }
319
329
  }
320
330
  if (manifest.warnings && manifest.warnings.length > 0) {
@@ -338,7 +348,7 @@ function displayStepByStepValidation(result) {
338
348
 
339
349
  displayApplicationStep(result.steps.application);
340
350
 
341
- if (result.steps.components.files && result.steps.components.files.length > 0) {
351
+ if (!result.steps.components.valid || (result.steps.components.files && result.steps.components.files.length > 0)) {
342
352
  displayComponentsStep(result.steps.components);
343
353
  }
344
354
 
@@ -356,6 +366,68 @@ function displayStepByStepValidation(result) {
356
366
  if (result.warnings && result.warnings.length > 0) {
357
367
  displayAggregatedWarnings(result.warnings);
358
368
  }
369
+
370
+ displayOverallStatus(result);
371
+ if (result.appPath) {
372
+ logger.log(chalk.gray(`\n${path.resolve(result.appPath)}`));
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Displays batch validation results (per-app blocks then summary).
378
+ * Expects batchResult.batch === true and batchResult.results.
379
+ * @function displayBatchValidationResults
380
+ * @param {Object} batchResult - Batch result from validateAllIntegrations / validateAllBuilderApps / validateAll
381
+ */
382
+ function displayBatchValidationResults(batchResult) {
383
+ if (!batchResult || batchResult.batch !== true || !Array.isArray(batchResult.results)) {
384
+ return;
385
+ }
386
+
387
+ const results = batchResult.results;
388
+ results.forEach(item => {
389
+ logger.log(chalk.blue(`\n--- ${item.appName} ---`));
390
+ if (item.error) {
391
+ logger.log(chalk.red(` ✗ ${item.error}`));
392
+ } else if (item.result) {
393
+ displayValidationResults(item.result);
394
+ }
395
+ });
396
+
397
+ const passed = results.filter(r => r.result && r.result.valid).length;
398
+ const failed = results.length - passed;
399
+ logger.log(chalk.blue('\n--- Summary ---'));
400
+ if (failed === 0) {
401
+ logger.log(chalk.green(`✓ ${passed} passed, 0 failed`));
402
+ logger.log(chalk.green('Overall: Passed'));
403
+ } else {
404
+ logger.log(chalk.red(`✗ ${passed} passed, ${failed} failed`));
405
+ logger.log(chalk.red('Overall: Failed'));
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Displays overall validation status
411
+ * @param {Object} result - Validation result
412
+ */
413
+ function displayOverallStatus(result) {
414
+ let hasErrors = result.errors && result.errors.length > 0;
415
+ if (!hasErrors && result.steps) {
416
+ const stepErrors = [result.steps.application, result.steps.components, result.steps.manifest]
417
+ .filter(Boolean)
418
+ .some(s => s.errors && s.errors.length > 0);
419
+ if (stepErrors) {
420
+ hasErrors = true;
421
+ }
422
+ }
423
+ const hasWarnings = result.warnings && result.warnings.length > 0;
424
+ if (hasErrors) {
425
+ logger.log(chalk.red('\nOverall: Failed'));
426
+ } else if (hasWarnings) {
427
+ logger.log(chalk.yellow('\nOverall: Passed with warnings'));
428
+ } else {
429
+ logger.log(chalk.green('\nOverall: Passed'));
430
+ }
359
431
  }
360
432
 
361
433
  /**
@@ -390,10 +462,16 @@ function displayValidationResults(result) {
390
462
  // Combine all warnings
391
463
  const allWarnings = [...(result.warnings || []), ...dimensionWarnings];
392
464
  displayAggregatedWarnings(allWarnings);
465
+
466
+ displayOverallStatus(result);
467
+ if (result.appPath) {
468
+ logger.log(chalk.gray(`\n${path.resolve(result.appPath)}`));
469
+ }
393
470
  }
394
471
 
395
472
  module.exports = {
396
473
  displayValidationResults,
474
+ displayBatchValidationResults,
397
475
  displayStepByStepValidation,
398
476
  displayApplicationValidation,
399
477
  displayExternalFilesValidation,