@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.
- package/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +8 -7
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +10 -5
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +207 -38
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +78 -37
- package/lib/app/prompts.js +9 -5
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +50 -32
- package/lib/cli.js +243 -65
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +261 -0
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +43 -29
- package/lib/commands/login.js +22 -13
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +129 -357
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +34 -23
- package/lib/datasource/list.js +8 -6
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +54 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +34 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +5 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +24 -5
- package/lib/schema/external-datasource.schema.json +303 -17
- package/lib/schema/external-system.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +37 -42
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/app-register-display.js +2 -1
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/cli-utils.js +3 -1
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +115 -0
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-map.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +149 -28
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +69 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +38 -4
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +19 -17
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/external-system/external-system.json.hbs +1 -1
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- 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
|
+
};
|
package/lib/utils/dev-config.js
CHANGED
|
@@ -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
|
|
package/lib/utils/env-copy.js
CHANGED
|
@@ -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
|
|
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);
|
package/lib/utils/env-map.js
CHANGED
package/lib/utils/env-ports.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
101
|
+
|
|
102
|
+
type: params.type
|
|
103
|
+
? `${field}: Expected ${params.type}, got ${typeof error.data}`
|
|
32
104
|
: `${field}: Type error`,
|
|
33
|
-
|
|
34
|
-
|
|
105
|
+
|
|
106
|
+
minimum: params.limit !== undefined
|
|
107
|
+
? `${field}: Value must be at least ${params.limit}`
|
|
35
108
|
: `${field}: Value below minimum`,
|
|
36
|
-
|
|
37
|
-
|
|
109
|
+
|
|
110
|
+
maximum: params.limit !== undefined
|
|
111
|
+
? `${field}: Value must be at most ${params.limit}`
|
|
38
112
|
: `${field}: Value above maximum`,
|
|
39
|
-
|
|
40
|
-
|
|
113
|
+
|
|
114
|
+
minLength: params.limit !== undefined
|
|
115
|
+
? `${field}: Must be at least ${params.limit} characters`
|
|
41
116
|
: `${field}: Too short`,
|
|
42
|
-
|
|
43
|
-
|
|
117
|
+
|
|
118
|
+
maxLength: params.limit !== undefined
|
|
119
|
+
? `${field}: Must be at most ${params.limit} characters`
|
|
44
120
|
: `${field}: Too long`,
|
|
45
|
-
|
|
46
|
-
enum:
|
|
47
|
-
? `${field}: Must be one of: ${
|
|
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
|
-
|
|
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
|
+
};
|