@aifabrix/builder 2.32.2 → 2.33.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 (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Controller URL Resolution Utilities
3
+ *
4
+ * Provides utilities for resolving controller URLs with developer ID-based defaults
5
+ * and fallback chain support.
6
+ *
7
+ * @fileoverview Controller URL resolution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { getDeveloperIdNumber } = require('./env-map');
13
+ const devConfig = require('./dev-config');
14
+ const config = require('../core/config');
15
+
16
+ /**
17
+ * Calculate default controller URL based on developer ID
18
+ * Uses getDevPorts to get the app port which is adjusted by developer ID
19
+ * Developer ID 0 = http://localhost:3000
20
+ * Developer ID 1 = http://localhost:3100
21
+ * Developer ID 2 = http://localhost:3200
22
+ * @async
23
+ * @function getDefaultControllerUrl
24
+ * @returns {Promise<string>} Default controller URL
25
+ */
26
+ async function getDefaultControllerUrl() {
27
+ const developerId = await getDeveloperIdNumber(null);
28
+ const ports = devConfig.getDevPorts(developerId);
29
+ return `http://localhost:${ports.app}`;
30
+ }
31
+
32
+ /**
33
+ * Normalize controller URL (remove trailing slashes)
34
+ * @param {string} url - Controller URL to normalize
35
+ * @returns {string} Normalized controller URL
36
+ */
37
+ function normalizeUrl(url) {
38
+ if (!url || typeof url !== 'string') {
39
+ return url;
40
+ }
41
+ return url.trim().replace(/\/+$/, '');
42
+ }
43
+
44
+ /**
45
+ * Get controller URL from logged-in user's device tokens
46
+ * Returns the first available controller URL from device tokens stored in config
47
+ * @async
48
+ * @function getControllerUrlFromLoggedInUser
49
+ * @returns {Promise<string|null>} Controller URL from logged-in user, or null if not found
50
+ */
51
+ async function getControllerUrlFromLoggedInUser() {
52
+ try {
53
+ const userConfig = await config.getConfig();
54
+ if (!userConfig.device || typeof userConfig.device !== 'object') {
55
+ return null;
56
+ }
57
+
58
+ const deviceUrls = Object.keys(userConfig.device);
59
+ if (deviceUrls.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ // Return the first available controller URL (normalized)
64
+ const firstControllerUrl = deviceUrls[0];
65
+ return normalizeUrl(firstControllerUrl);
66
+ } catch (error) {
67
+ // If config doesn't exist or can't be read, return null
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get controller URL from config.yaml
74
+ * @async
75
+ * @function getControllerFromConfig
76
+ * @returns {Promise<string|null>} Controller URL from config or null
77
+ */
78
+ async function getControllerFromConfig() {
79
+ const { getControllerUrl } = require('../core/config');
80
+ return await getControllerUrl();
81
+ }
82
+
83
+ /**
84
+ * Resolve controller URL with fallback chain
85
+ * Priority:
86
+ * 1. config.controller (from config.yaml)
87
+ * 2. getControllerUrlFromLoggedInUser() (from logged-in device tokens)
88
+ * 3. getDefaultControllerUrl() (developer ID-based default)
89
+ * @async
90
+ * @function resolveControllerUrl
91
+ * @returns {Promise<string>} Resolved controller URL
92
+ */
93
+ async function resolveControllerUrl() {
94
+ // Priority 1: config.controller (from config.yaml)
95
+ const configController = await getControllerFromConfig();
96
+ if (configController) {
97
+ return configController.replace(/\/+$/, '');
98
+ }
99
+
100
+ // Priority 2: Logged-in user's device tokens
101
+ const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
102
+ if (loggedInControllerUrl) {
103
+ return loggedInControllerUrl;
104
+ }
105
+
106
+ // Priority 3: Developer ID-based default
107
+ return await getDefaultControllerUrl();
108
+ }
109
+
110
+ module.exports = {
111
+ getDefaultControllerUrl,
112
+ getControllerUrlFromLoggedInUser,
113
+ getControllerFromConfig,
114
+ resolveControllerUrl
115
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Dataplane Health Check Utilities
3
+ *
4
+ * Provides utilities for checking dataplane URL health and reachability
5
+ *
6
+ * @fileoverview Dataplane health check utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Tests a single endpoint for reachability
13
+ * @async
14
+ * @function testEndpoint
15
+ * @param {string} testUrl - URL to test
16
+ * @param {number} timeoutMs - Timeout in milliseconds
17
+ * @returns {Promise<boolean>} True if endpoint is reachable
18
+ */
19
+ async function testEndpoint(testUrl, timeoutMs) {
20
+ try {
21
+ const controller = new AbortController();
22
+ const abortTimeoutId = setTimeout(() => controller.abort(), timeoutMs);
23
+ let raceTimeoutId;
24
+
25
+ const response = await Promise.race([
26
+ fetch(testUrl, {
27
+ method: 'GET',
28
+ signal: controller.signal,
29
+ headers: { 'Accept': 'application/json' }
30
+ }),
31
+ new Promise((_, reject) => {
32
+ raceTimeoutId = setTimeout(() => reject(new Error('Timeout')), timeoutMs);
33
+ })
34
+ ]).catch(() => null);
35
+
36
+ clearTimeout(abortTimeoutId);
37
+ if (raceTimeoutId) {
38
+ clearTimeout(raceTimeoutId);
39
+ }
40
+
41
+ // If we get any response (even 404 or 401), the service is reachable
42
+ // 401 is OK because it means the API is working, just needs auth
43
+ if (response && (response.ok || response.status === 404 || response.status === 401)) {
44
+ return true;
45
+ }
46
+
47
+ // If we get a 500 or other server error, the service is up but broken
48
+ // Still consider it "reachable" - the wizard will handle the actual error
49
+ if (response && response.status >= 500) {
50
+ return true;
51
+ }
52
+
53
+ // If response is null (timeout/error), endpoint is not reachable
54
+ return false;
55
+ } catch (error) {
56
+ // Timeout or abort means endpoint is not reachable
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Checks if dataplane URL is reachable and API is functional
63
+ * @async
64
+ * @function checkDataplaneHealth
65
+ * @param {string} dataplaneUrl - Dataplane URL to check
66
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 5000)
67
+ * @returns {Promise<boolean>} True if dataplane is reachable and functional
68
+ */
69
+ async function checkDataplaneHealth(dataplaneUrl, timeoutMs = 5000) {
70
+ if (!dataplaneUrl) {
71
+ return false;
72
+ }
73
+
74
+ const baseUrl = dataplaneUrl.replace(/\/+$/, '');
75
+ const endpointsToTest = ['/health', '/api/v1/health', '/api/health', ''];
76
+
77
+ for (const endpoint of endpointsToTest) {
78
+ const testUrl = endpoint ? `${baseUrl}${endpoint}` : baseUrl;
79
+ const isReachable = await testEndpoint(testUrl, timeoutMs);
80
+ if (isReachable) {
81
+ return true;
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Validates dataplane health before running wizard
90
+ * @async
91
+ * @function validateDataplaneHealth
92
+ * @param {string} dataplaneUrl - Dataplane URL to check
93
+ * @returns {Promise<Error|null>} Error if unhealthy, null if healthy
94
+ */
95
+ async function validateDataplaneHealth(dataplaneUrl) {
96
+ try {
97
+ const isHealthy = await checkDataplaneHealth(dataplaneUrl, 5000);
98
+ if (!isHealthy) {
99
+ return new Error(
100
+ `Dataplane is not reachable at ${dataplaneUrl}.\n\n` +
101
+ 'Please ensure the dataplane service is running and accessible, then try again.\n' +
102
+ `You can check dataplane status with: curl ${dataplaneUrl}/health`
103
+ );
104
+ }
105
+ return null;
106
+ } catch (error) {
107
+ return new Error(`Failed to check dataplane health: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ checkDataplaneHealth,
113
+ validateDataplaneHealth,
114
+ testEndpoint
115
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Dataplane URL Resolver
3
+ *
4
+ * Resolves dataplane URL by discovering from controller
5
+ *
6
+ * @fileoverview Dataplane URL resolution utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
12
+
13
+ /**
14
+ * Resolve dataplane URL by discovering from controller
15
+ * @async
16
+ * @function resolveDataplaneUrl
17
+ * @param {string} controllerUrl - Controller URL
18
+ * @param {string} environment - Environment key
19
+ * @param {Object} authConfig - Authentication configuration
20
+ * @returns {Promise<string>} Resolved dataplane URL
21
+ * @throws {Error} If dataplane URL cannot be resolved
22
+ */
23
+ async function resolveDataplaneUrl(controllerUrl, environment, authConfig) {
24
+ return await discoverDataplaneUrl(controllerUrl, environment, authConfig);
25
+ }
26
+
27
+ module.exports = {
28
+ resolveDataplaneUrl
29
+ };
@@ -18,7 +18,9 @@ const BASE_PORTS = {
18
18
  postgres: 5432,
19
19
  redis: 6379,
20
20
  pgadmin: 5050,
21
- redisCommander: 8081
21
+ redisCommander: 8081,
22
+ traefikHttp: 80,
23
+ traefikHttps: 443
22
24
  };
23
25
 
24
26
  /**
@@ -65,7 +67,9 @@ function getDevPorts(developerId) {
65
67
  postgres: BASE_PORTS.postgres + offset,
66
68
  redis: BASE_PORTS.redis + offset,
67
69
  pgadmin: BASE_PORTS.pgadmin + offset,
68
- redisCommander: BASE_PORTS.redisCommander + offset
70
+ redisCommander: BASE_PORTS.redisCommander + offset,
71
+ traefikHttp: BASE_PORTS.traefikHttp + offset,
72
+ traefikHttps: BASE_PORTS.traefikHttps + offset
69
73
  };
70
74
  }
71
75
 
@@ -18,6 +18,7 @@ const devConfig = require('../utils/dev-config');
18
18
  const { rewriteInfraEndpoints } = require('./env-endpoints');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { interpolateEnvVars } = require('./secrets-helpers');
21
+ const { getLocalPort } = require('./port-resolver');
21
22
 
22
23
  /**
23
24
  * Read developer ID from config file synchronously
@@ -157,7 +158,7 @@ function extractEnvVarsFromContent(envContent, envVars) {
157
158
  * @returns {Promise<string>} Patched env content
158
159
  */
159
160
  async function patchEnvContentForLocal(envContent, variables) {
160
- const baseAppPort = variables.build?.localPort || variables.port || 3000;
161
+ const baseAppPort = getLocalPort(variables, 3000);
161
162
  const appPort = calculateDevAppPort(baseAppPort);
162
163
  const devIdNum = readDeveloperIdFromConfig(config) || 0;
163
164
  const infraPorts = devConfig.getDevPorts(devIdNum);
@@ -297,6 +297,7 @@ async function buildEnvVarMap(context, osModule = null, developerId = null) {
297
297
  }
298
298
 
299
299
  module.exports = {
300
- buildEnvVarMap
300
+ buildEnvVarMap,
301
+ getDeveloperIdNumber
301
302
  };
302
303
 
@@ -11,6 +11,7 @@
11
11
  const fs = require('fs');
12
12
  const yaml = require('js-yaml');
13
13
  const config = require('../core/config');
14
+ const { getLocalPort } = require('./port-resolver');
14
15
 
15
16
  /**
16
17
  * Update PORT in the container's .env file to use variables.port (+offset)
@@ -66,7 +67,7 @@ function updateContainerPortInEnvFile(envPath, variablesPath) {
66
67
  }
67
68
  const variablesContent = fs.readFileSync(variablesPath, 'utf8');
68
69
  const variables = yaml.load(variablesContent);
69
- const basePort = variables?.port || 3000;
70
+ const basePort = getLocalPort(variables, 3000);
70
71
  const devIdNum = getDeveloperIdFromEnvOrConfig();
71
72
  const port = calculatePortWithDevId(basePort, devIdNum);
72
73
  let envContent = fs.readFileSync(envPath, 'utf8');
@@ -20,7 +20,7 @@ const logger = require('../utils/logger');
20
20
  * @param {string} appKey - Application key
21
21
  * @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
22
22
  * @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
23
- * @param {string} _controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
23
+ * @param {string} _controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.dev')
24
24
  * Note: This parameter is accepted for compatibility but the template format http://${MISO_HOST}:${MISO_PORT} is used instead
25
25
  * @returns {Promise<void>} Resolves when template is updated
26
26
  */
@@ -10,45 +10,145 @@
10
10
  */
11
11
 
12
12
  /**
13
- * Formats a single validation error into a developer-friendly message
14
- *
15
- * @function formatSingleError
16
- * @param {Object} error - Raw validation error from Ajv
17
- * @returns {string} Formatted error message
13
+ * Maps common regex patterns to human-readable descriptions
14
+ * @type {Object.<string, string>}
18
15
  */
19
- function formatSingleError(error) {
20
- // Handle empty or missing instancePath - use 'Configuration' for root level errors
16
+ const PATTERN_DESCRIPTIONS = {
17
+ '^[a-z0-9-]+$': 'lowercase letters, numbers, and hyphens only',
18
+ '^[a-z0-9-:]+$': 'lowercase letters, numbers, hyphens, and colons only (e.g., "entity:action")',
19
+ '^[a-z-]+$': 'lowercase letters and hyphens only',
20
+ '^[A-Z_][A-Z0-9_]*$': 'uppercase letters, numbers, and underscores (must start with letter or underscore)',
21
+ '^[a-zA-Z0-9_-]+$': 'letters, numbers, hyphens, and underscores only',
22
+ '^(http|https)://.*$': 'valid HTTP or HTTPS URL',
23
+ '^/[a-z0-9/-]*$': 'URL path starting with / (lowercase letters, numbers, hyphens, slashes)'
24
+ };
25
+
26
+ /**
27
+ * Gets a human-readable description of a regex pattern
28
+ * @function getPatternDescription
29
+ * @param {string} pattern - The regex pattern
30
+ * @returns {string} Human-readable description
31
+ */
32
+ function getPatternDescription(pattern) {
33
+ return PATTERN_DESCRIPTIONS[pattern] || `must match pattern: ${pattern}`;
34
+ }
35
+
36
+ /**
37
+ * Extracts the field name from an error's instancePath
38
+ * @function getFieldName
39
+ * @param {Object} error - Validation error object
40
+ * @returns {string} Formatted field name
41
+ */
42
+ function getFieldName(error) {
21
43
  const instancePath = error.instancePath || '';
22
44
  const path = instancePath ? instancePath.slice(1) : '';
23
- const field = path ? `Field "${path}"` : 'Configuration';
45
+ return path ? `Field "${path}"` : 'Configuration';
46
+ }
47
+
48
+ /**
49
+ * Formats a pattern validation error with the actual invalid value
50
+ * @function formatPatternError
51
+ * @param {string} field - Field name
52
+ * @param {Object} error - Validation error object
53
+ * @returns {string} Formatted error message
54
+ */
55
+ function formatPatternError(field, error) {
56
+ const invalidValue = error.data !== undefined ? `"${error.data}"` : 'value';
57
+ const patternDesc = getPatternDescription(error.params?.pattern);
58
+ return `${field}: Invalid value ${invalidValue} - ${patternDesc}`;
59
+ }
60
+
61
+ /**
62
+ * Formats additionalProperties validation errors
63
+ * @function formatAdditionalPropertiesError
64
+ * @param {string} field - Field name
65
+ * @param {Object} error - Validation error object
66
+ * @returns {string} Formatted error message
67
+ */
68
+ function formatAdditionalPropertiesError(field, error) {
69
+ const invalidProperty = error.params?.additionalProperty;
70
+ const parentSchema = error.parentSchema || {};
71
+ const allowedProps = parentSchema.properties ? Object.keys(parentSchema.properties) : [];
72
+ const lines = [`${field}: must NOT have additional properties`];
73
+
74
+ if (invalidProperty) {
75
+ lines.push(` Invalid property: "${invalidProperty}" (not allowed)`);
76
+ }
77
+ if (allowedProps.length > 0) {
78
+ lines.push(` Allowed properties: ${allowedProps.join(', ')}`);
79
+ }
80
+ if ((error.instancePath || '').includes('/portalInput/validation')) {
81
+ lines.push(' Example: { "minLength": 1, "maxLength": 1000, "pattern": "^[0-9]+$", "required": false }');
82
+ }
83
+
84
+ return lines.join('\n');
85
+ }
24
86
 
25
- // Check if params exists before accessing it
26
- const errorMessages = {
27
- required: error.params?.missingProperty
28
- ? `${field}: Missing required property "${error.params.missingProperty}"`
87
+ /**
88
+ * Creates error message formatters for each validation keyword
89
+ * @function createKeywordFormatters
90
+ * @param {string} field - Field name
91
+ * @param {Object} error - Validation error object
92
+ * @returns {Object} Object mapping keywords to formatted messages
93
+ */
94
+ function createKeywordFormatters(field, error) {
95
+ const params = error.params || {};
96
+
97
+ return {
98
+ required: params.missingProperty
99
+ ? `${field}: Missing required property "${params.missingProperty}"`
29
100
  : `${field}: Missing required property`,
30
- type: error.params?.type
31
- ? `${field}: Expected ${error.params.type}, got ${typeof error.data}`
101
+
102
+ type: params.type
103
+ ? `${field}: Expected ${params.type}, got ${typeof error.data}`
32
104
  : `${field}: Type error`,
33
- minimum: error.params?.limit
34
- ? `${field}: Value must be at least ${error.params.limit}`
105
+
106
+ minimum: params.limit !== undefined
107
+ ? `${field}: Value must be at least ${params.limit}`
35
108
  : `${field}: Value below minimum`,
36
- maximum: error.params?.limit
37
- ? `${field}: Value must be at most ${error.params.limit}`
109
+
110
+ maximum: params.limit !== undefined
111
+ ? `${field}: Value must be at most ${params.limit}`
38
112
  : `${field}: Value above maximum`,
39
- minLength: error.params?.limit
40
- ? `${field}: Must be at least ${error.params.limit} characters`
113
+
114
+ minLength: params.limit !== undefined
115
+ ? `${field}: Must be at least ${params.limit} characters`
41
116
  : `${field}: Too short`,
42
- maxLength: error.params?.limit
43
- ? `${field}: Must be at most ${error.params.limit} characters`
117
+
118
+ maxLength: params.limit !== undefined
119
+ ? `${field}: Must be at most ${params.limit} characters`
44
120
  : `${field}: Too long`,
45
- pattern: `${field}: Invalid format`,
46
- enum: error.params?.allowedValues && error.params.allowedValues.length > 0
47
- ? `${field}: Must be one of: ${error.params.allowedValues.join(', ')}`
121
+
122
+ enum: params.allowedValues && params.allowedValues.length > 0
123
+ ? `${field}: Must be one of: ${params.allowedValues.join(', ')}`
48
124
  : `${field}: Must be one of: unknown`
49
125
  };
126
+ }
50
127
 
51
- return errorMessages[error.keyword] || `${field}: ${error.message || 'Validation error'}`;
128
+ /**
129
+ * Formats a single validation error into a developer-friendly message
130
+ *
131
+ * @function formatSingleError
132
+ * @param {Object} error - Raw validation error from Ajv
133
+ * @returns {string} Formatted error message
134
+ */
135
+ function formatSingleError(error) {
136
+ const field = getFieldName(error);
137
+
138
+ // Handle pattern errors with special formatting
139
+ if (error.keyword === 'pattern') {
140
+ return formatPatternError(field, error);
141
+ }
142
+ if (error.keyword === 'additionalProperties') {
143
+ return formatAdditionalPropertiesError(field, error);
144
+ }
145
+
146
+ // Use object lookup for keyword-specific messages
147
+ const formatters = createKeywordFormatters(field, error);
148
+ const message = formatters[error.keyword];
149
+
150
+ // Return keyword message or fallback to generic message
151
+ return message || `${field}: ${error.message || 'Validation error'}`;
52
152
  }
53
153
 
54
154
  /**
@@ -71,8 +171,29 @@ function formatValidationErrors(errors) {
71
171
  return errors.map(formatSingleError);
72
172
  }
73
173
 
174
+ /**
175
+ * Formats the error when a required DB password variable is missing.
176
+ * Supports single-db (DB_0_PASSWORD or DB_PASSWORD) and multi-db (DB_0_PASSWORD, DB_1_PASSWORD, ...).
177
+ * @param {string} appKey - Application key
178
+ * @param {Object} opts - Options
179
+ * @param {boolean} [opts.multiDb] - True when multiple databases; required passwordKey is used, no hardcoded index
180
+ * @param {string} [opts.passwordKey] - The missing variable name (e.g. 'DB_1_PASSWORD'); required when multiDb is true
181
+ * @returns {string} Error message with next steps
182
+ */
183
+ function formatMissingDbPasswordError(appKey, opts = {}) {
184
+ const { multiDb, passwordKey } = opts;
185
+ if (multiDb && passwordKey) {
186
+ return 'Missing required password variable ' + passwordKey + ' in .env file for application \'' + appKey + '\'. ' +
187
+ 'Add ' + passwordKey + '=your_secret to your .env file. For multiple databases you need DB_0_PASSWORD, DB_1_PASSWORD, etc.';
188
+ }
189
+ return 'Missing required password variable DB_0_PASSWORD or DB_PASSWORD in .env file for application \'' + appKey + '\'. ' +
190
+ 'This app has requires.database or databases in variables.yaml. Add DB_0_PASSWORD=your_secret or DB_PASSWORD=your_secret to builder/' + appKey + '/.env (or run \'aifabrix resolve ' + appKey + '\'), or set requires.database: false in variables.yaml if not needed.';
191
+ }
192
+
74
193
  module.exports = {
75
194
  formatSingleError,
76
- formatValidationErrors
195
+ formatValidationErrors,
196
+ formatMissingDbPasswordError,
197
+ getPatternDescription,
198
+ PATTERN_DESCRIPTIONS
77
199
  };
78
-
@@ -0,0 +1,125 @@
1
+ /**
2
+ * External System README Generation
3
+ *
4
+ * Provides a shared Handlebars-based README generator for external systems.
5
+ *
6
+ * @fileoverview External system README generation utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const handlebars = require('handlebars');
16
+ const { getProjectRoot } = require('./paths');
17
+
18
+ /**
19
+ * Formats a display name from a key
20
+ * @param {string} key - System or app key
21
+ * @returns {string} Display name
22
+ */
23
+ function formatDisplayName(key) {
24
+ if (!key || typeof key !== 'string') {
25
+ return 'External System';
26
+ }
27
+ return key
28
+ .split('-')
29
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
30
+ .join(' ');
31
+ }
32
+
33
+ /**
34
+ * Normalizes datasource entries for template use
35
+ * @param {Array} datasources - Datasource objects
36
+ * @param {string} systemKey - System key for filename generation
37
+ * @returns {Array<{entityType: string, displayName: string, fileName: string}>} Normalized entries
38
+ */
39
+ function normalizeDatasources(datasources, systemKey) {
40
+ if (!Array.isArray(datasources)) {
41
+ return [];
42
+ }
43
+ return datasources.map((datasource, index) => {
44
+ const entityType = datasource.entityType ||
45
+ datasource.entityKey ||
46
+ datasource.key?.split('-').pop() ||
47
+ `entity${index + 1}`;
48
+ const displayName = datasource.displayName ||
49
+ datasource.name ||
50
+ `Datasource ${index + 1}`;
51
+ let fileName = datasource.fileName || datasource.file;
52
+ if (!fileName) {
53
+ const key = datasource.key || '';
54
+ // Extract entity from keys like "hubspot-deploy-company" -> "company"
55
+ const entity = (systemKey && key.startsWith(`${systemKey}-deploy-`))
56
+ ? key.slice(`${systemKey}-deploy-`.length)
57
+ : entityType;
58
+ fileName = systemKey ? `${systemKey}-datasource-${entity}.json` : `${entity}.json`;
59
+ }
60
+ return { entityType, displayName, fileName };
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Builds the external system README template context
66
+ * @function buildExternalReadmeContext
67
+ * @param {Object} params - Context parameters
68
+ * @param {string} [params.appName] - Application name
69
+ * @param {string} [params.systemKey] - System key
70
+ * @param {string} [params.systemType] - System type
71
+ * @param {string} [params.displayName] - Display name
72
+ * @param {string} [params.description] - Description
73
+ * @param {Array} [params.datasources] - Datasource objects
74
+ * @returns {Object} Template context
75
+ */
76
+ function buildExternalReadmeContext(params = {}) {
77
+ const appName = params.appName || params.systemKey || 'external-system';
78
+ const systemKey = params.systemKey || appName;
79
+ const displayName = params.displayName || formatDisplayName(systemKey);
80
+ const description = params.description || `External system integration for ${systemKey}`;
81
+ const systemType = params.systemType || 'openapi';
82
+ const datasources = normalizeDatasources(params.datasources, systemKey);
83
+
84
+ return {
85
+ appName,
86
+ systemKey,
87
+ displayName,
88
+ description,
89
+ systemType,
90
+ datasourceCount: datasources.length,
91
+ datasources
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Loads and compiles the external system README template
97
+ * @returns {Function} Compiled template
98
+ * @throws {Error} If template is missing
99
+ */
100
+ function loadExternalReadmeTemplate() {
101
+ const projectRoot = getProjectRoot();
102
+ const templatePath = path.join(projectRoot, 'templates', 'external-system', 'README.md.hbs');
103
+ if (!fs.existsSync(templatePath)) {
104
+ throw new Error(`External system README template not found at ${templatePath}`);
105
+ }
106
+ const content = fs.readFileSync(templatePath, 'utf8');
107
+ return handlebars.compile(content);
108
+ }
109
+
110
+ /**
111
+ * Generates README content for an external system
112
+ * @function generateExternalReadmeContent
113
+ * @param {Object} params - Context parameters
114
+ * @returns {string} README content
115
+ */
116
+ function generateExternalReadmeContent(params = {}) {
117
+ const template = loadExternalReadmeTemplate();
118
+ const context = buildExternalReadmeContext(params);
119
+ return template(context);
120
+ }
121
+
122
+ module.exports = {
123
+ buildExternalReadmeContext,
124
+ generateExternalReadmeContent
125
+ };