@aifabrix/builder 2.36.0 → 2.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/integration/hubspot/README.md +47 -0
- package/integration/hubspot/env.template +6 -1
- package/integration/hubspot/hubspot-deploy.json +0 -6
- package/integration/hubspot/hubspot-system.json +0 -6
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
- package/integration/hubspot/test-dataplane-down-helpers.js +4 -1
- package/integration/hubspot/test-dataplane-down-tests.js +5 -33
- package/integration/hubspot/test-dataplane-down.js +60 -8
- package/integration/hubspot/test.js +53 -19
- package/integration/hubspot/wizard-hubspot-e2e.yaml +1 -1
- package/lib/app/config.js +0 -1
- package/lib/app/show-display.js +27 -1
- package/lib/commands/app.js +15 -0
- package/lib/commands/wizard-helpers.js +13 -4
- package/lib/commands/wizard.js +1 -0
- package/lib/generator/wizard-prompts.js +13 -1
- package/lib/schema/env-config.yaml +1 -3
- package/lib/utils/token-manager.js +30 -22
- package/package.json +1 -1
- package/integration/test-hubspot/wizard.yaml +0 -8
|
@@ -128,9 +128,56 @@ aifabrix datasource list
|
|
|
128
128
|
aifabrix datasource validate hubspot-company
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## Wizard E2E tests and use case coverage
|
|
132
|
+
|
|
133
|
+
The script `integration/hubspot/test.js` runs end-to-end wizard and validation tests. It covers all wizard use cases from [Wizard Guide](../../docs/wizard.md):
|
|
134
|
+
|
|
135
|
+
| Use case | Test ID | Description |
|
|
136
|
+
|----------|---------|-------------|
|
|
137
|
+
| **Headless config (wizard.yaml)** | | |
|
|
138
|
+
| Required `appName` | 2.1 | Rejects config missing `appName` |
|
|
139
|
+
| Valid `appName` pattern (lowercase, hyphens/underscores) | 2.2 | Rejects uppercase in app name |
|
|
140
|
+
| Required `mode` | 2.5 | Rejects invalid `mode` enum |
|
|
141
|
+
| Required `source` block | 2.3 | Rejects config without `source` |
|
|
142
|
+
| Valid `source.type` | 2.4 | Rejects invalid source type |
|
|
143
|
+
| `openapi-file` requires `filePath` | 2.7 | Rejects missing/non-existent OpenAPI file |
|
|
144
|
+
| `openapi-url` requires `url` | 2.8 | Rejects `openapi-url` without `url` |
|
|
145
|
+
| `known-platform` requires `platform` | 2.6 | Rejects known-platform without `platform` |
|
|
146
|
+
| **Mode: add-datasource** | | |
|
|
147
|
+
| `systemIdOrKey` required when mode=add-datasource | 2.9 | Rejects add-datasource without systemIdOrKey |
|
|
148
|
+
| **Credential** | | |
|
|
149
|
+
| `credential.action=select` requires `credentialIdOrKey` | 2.10 | Rejects select without credentialIdOrKey |
|
|
150
|
+
| `credential.action=create` requires `config` | 2.11 | Rejects create without config |
|
|
151
|
+
| **Positive flows** | | |
|
|
152
|
+
| Full wizard with OpenAPI file | 1.1 | Complete flow with local OpenAPI file |
|
|
153
|
+
| Wizard with known platform | 1.2 | Flow using known-platform (e.g. HubSpot) |
|
|
154
|
+
| Wizard with env var substitution in deployment | 1.6 | `${CONTROLLER_URL}`, `${DATAPLANE_URL}` in wizard.yaml |
|
|
155
|
+
| Real credential creation (real-data) | 1.3 | Credential create with real OAuth2 (optional env) |
|
|
156
|
+
| **Post-wizard validation (external system)** | | |
|
|
157
|
+
| RBAC: permissions reference existing roles | 2.12 | Rejects permission referencing non-existent role |
|
|
158
|
+
| RBAC: rbac.yaml valid YAML and structure | 2.13 | Rejects invalid YAML in rbac.yaml |
|
|
159
|
+
| Datasource: dimensions required in fieldMappings | 2.14 | Rejects missing dimensions |
|
|
160
|
+
| Datasource: dimension key pattern | 2.15 | Rejects invalid dimension key |
|
|
161
|
+
| Datasource: attribute path pattern | 2.16 | Rejects invalid attribute path |
|
|
162
|
+
| Datasource: dimensions must be object | 2.17 | Rejects dimensions as array |
|
|
163
|
+
|
|
164
|
+
**Run tests:**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# All tests (positive may skip if dataplane/controller unavailable)
|
|
168
|
+
node integration/hubspot/test.js
|
|
169
|
+
|
|
170
|
+
# Negative only (no dataplane required)
|
|
171
|
+
node integration/hubspot/test.js --type negative
|
|
172
|
+
|
|
173
|
+
# Specific test
|
|
174
|
+
node integration/hubspot/test.js --test "2.1,2.2"
|
|
175
|
+
```
|
|
176
|
+
|
|
131
177
|
## Documentation
|
|
132
178
|
|
|
133
179
|
- [External Systems Guide](../../docs/external-systems.md) - Complete guide with examples
|
|
180
|
+
- [Wizard Guide](../../docs/wizard.md) - Wizard workflow and headless config
|
|
134
181
|
- [CLI Reference](../../docs/cli-reference.md) - All commands
|
|
135
182
|
- [Configuration Reference](../../docs/CONFIGURATION.md) - Config file details
|
|
136
183
|
|
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
CLIENTID=kv://hubspot-clientidKeyVault
|
|
6
6
|
CLIENTSECRET=kv://hubspot-clientsecretKeyVault
|
|
7
7
|
TOKENURL=https://api.hubapi.com/oauth/v1/token
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
# Optional: test runner (integration/hubspot/test.js) reads all config from this .env
|
|
10
|
+
# CONTROLLER_URL=http://localhost:3110
|
|
11
|
+
# ENVIRONMENT=miso
|
|
12
|
+
# DATAPLANE_URL=
|
|
13
|
+
# HUBSPOT_OPENAPI_FILE=integration/hubspot/companies.json
|
|
9
14
|
|
|
@@ -217,7 +217,10 @@ function validateWizardResult(result, output) {
|
|
|
217
217
|
'fetch failed',
|
|
218
218
|
'timeout',
|
|
219
219
|
'unreachable',
|
|
220
|
-
'failed to create wizard session'
|
|
220
|
+
'failed to create wizard session',
|
|
221
|
+
'authentication failed',
|
|
222
|
+
'no authentication method',
|
|
223
|
+
'client token unavailable'
|
|
221
224
|
];
|
|
222
225
|
|
|
223
226
|
const isValid = !result.success && validateError(output, expectedPatterns);
|
|
@@ -65,13 +65,7 @@ async function testWizard() {
|
|
|
65
65
|
'bin/aifabrix.js',
|
|
66
66
|
'wizard',
|
|
67
67
|
'--config',
|
|
68
|
-
configPath
|
|
69
|
-
'--controller',
|
|
70
|
-
CONTROLLER_URL,
|
|
71
|
-
'--environment',
|
|
72
|
-
ENVIRONMENT,
|
|
73
|
-
'--dataplane',
|
|
74
|
-
INVALID_DATAPLANE_URL
|
|
68
|
+
configPath
|
|
75
69
|
];
|
|
76
70
|
|
|
77
71
|
const result = await runCommand('node', args);
|
|
@@ -107,11 +101,7 @@ async function testDownload() {
|
|
|
107
101
|
const args = [
|
|
108
102
|
'bin/aifabrix.js',
|
|
109
103
|
'download',
|
|
110
|
-
'non-existent-system-that-should-fail'
|
|
111
|
-
'--environment',
|
|
112
|
-
ENVIRONMENT,
|
|
113
|
-
'--controller',
|
|
114
|
-
CONTROLLER_URL
|
|
104
|
+
'non-existent-system-that-should-fail'
|
|
115
105
|
];
|
|
116
106
|
|
|
117
107
|
const result = await runCommand('node', args);
|
|
@@ -155,10 +145,6 @@ async function testDelete() {
|
|
|
155
145
|
'non-existent-system-that-should-fail',
|
|
156
146
|
'--type',
|
|
157
147
|
'external',
|
|
158
|
-
'--environment',
|
|
159
|
-
ENVIRONMENT,
|
|
160
|
-
'--controller',
|
|
161
|
-
CONTROLLER_URL,
|
|
162
148
|
'--yes'
|
|
163
149
|
];
|
|
164
150
|
|
|
@@ -219,13 +205,7 @@ function buildDatasourceDeployArgs(datasourcePath) {
|
|
|
219
205
|
'datasource',
|
|
220
206
|
'deploy',
|
|
221
207
|
'test-app',
|
|
222
|
-
datasourcePath
|
|
223
|
-
'--environment',
|
|
224
|
-
ENVIRONMENT,
|
|
225
|
-
'--controller',
|
|
226
|
-
CONTROLLER_URL,
|
|
227
|
-
'--dataplane',
|
|
228
|
-
INVALID_DATAPLANE_URL
|
|
208
|
+
datasourcePath
|
|
229
209
|
];
|
|
230
210
|
}
|
|
231
211
|
|
|
@@ -321,11 +301,7 @@ async function testIntegration() {
|
|
|
321
301
|
const args = [
|
|
322
302
|
'bin/aifabrix.js',
|
|
323
303
|
'test-integration',
|
|
324
|
-
testApp
|
|
325
|
-
'--environment',
|
|
326
|
-
ENVIRONMENT,
|
|
327
|
-
'--controller',
|
|
328
|
-
CONTROLLER_URL
|
|
304
|
+
testApp
|
|
329
305
|
];
|
|
330
306
|
|
|
331
307
|
const result = await runCommand('node', args);
|
|
@@ -372,11 +348,7 @@ async function testDataplaneDiscovery() {
|
|
|
372
348
|
const args = [
|
|
373
349
|
'bin/aifabrix.js',
|
|
374
350
|
'download',
|
|
375
|
-
'non-existent-system-for-discovery-test'
|
|
376
|
-
'--environment',
|
|
377
|
-
ENVIRONMENT,
|
|
378
|
-
'--controller',
|
|
379
|
-
CONTROLLER_URL
|
|
351
|
+
'non-existent-system-for-discovery-test'
|
|
380
352
|
];
|
|
381
353
|
|
|
382
354
|
const result = await runCommand('node', args);
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
14
17
|
const {
|
|
15
18
|
logInfo,
|
|
16
19
|
logSuccess,
|
|
@@ -27,9 +30,51 @@ const {
|
|
|
27
30
|
} = require('./test-dataplane-down-tests');
|
|
28
31
|
|
|
29
32
|
const INVALID_DATAPLANE_URL = 'http://localhost:9999';
|
|
30
|
-
const CONTROLLER_URL = process.env.CONTROLLER_URL || 'http://localhost:3110';
|
|
31
33
|
const ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
|
|
32
34
|
|
|
35
|
+
/** Path to temp config used so CLI uses invalid controller URL and fails with connection error */
|
|
36
|
+
let tempConfigPath = null;
|
|
37
|
+
let originalAifabrixConfig = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create temp config pointing controller to invalid URL so commands fail with connection error
|
|
41
|
+
* @async
|
|
42
|
+
* @returns {Promise<string>} Path to temp config file
|
|
43
|
+
*/
|
|
44
|
+
async function createTempConfig() {
|
|
45
|
+
const dir = path.join(os.tmpdir(), `aifabrix-dataplane-down-${Date.now()}`);
|
|
46
|
+
await fs.mkdir(dir, { recursive: true });
|
|
47
|
+
const configPath = path.join(dir, 'config.yaml');
|
|
48
|
+
const content = `controller: "${INVALID_DATAPLANE_URL}"
|
|
49
|
+
environment: "${ENVIRONMENT}"
|
|
50
|
+
`;
|
|
51
|
+
await fs.writeFile(configPath, content, 'utf8');
|
|
52
|
+
return configPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Restore AIFABRIX_CONFIG and remove temp config
|
|
57
|
+
* @async
|
|
58
|
+
* @returns {Promise<void>} Resolves when cleanup is complete
|
|
59
|
+
*/
|
|
60
|
+
async function cleanupTempConfig() {
|
|
61
|
+
if (originalAifabrixConfig !== undefined) {
|
|
62
|
+
if (originalAifabrixConfig === null) {
|
|
63
|
+
delete process.env.AIFABRIX_CONFIG;
|
|
64
|
+
} else {
|
|
65
|
+
process.env.AIFABRIX_CONFIG = originalAifabrixConfig;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (tempConfigPath) {
|
|
69
|
+
try {
|
|
70
|
+
await fs.rm(path.dirname(tempConfigPath), { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore cleanup errors
|
|
73
|
+
}
|
|
74
|
+
tempConfigPath = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
33
78
|
/**
|
|
34
79
|
* Displays failed test details
|
|
35
80
|
* @function displayFailedTestDetails
|
|
@@ -137,17 +182,24 @@ async function main() {
|
|
|
137
182
|
logInfo('='.repeat(60));
|
|
138
183
|
logInfo('Dataplane Down Error Handling Test Suite');
|
|
139
184
|
logInfo('='.repeat(60));
|
|
140
|
-
logInfo(`Invalid
|
|
141
|
-
logInfo(`Controller URL: ${CONTROLLER_URL}`);
|
|
185
|
+
logInfo(`Invalid Controller URL (used for all commands): ${INVALID_DATAPLANE_URL}`);
|
|
142
186
|
logInfo(`Environment: ${ENVIRONMENT}`);
|
|
143
187
|
logInfo('='.repeat(60));
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
try {
|
|
190
|
+
tempConfigPath = await createTempConfig();
|
|
191
|
+
originalAifabrixConfig = process.env.AIFABRIX_CONFIG || null;
|
|
192
|
+
process.env.AIFABRIX_CONFIG = tempConfigPath;
|
|
147
193
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
const results = await runTests();
|
|
195
|
+
displaySummary(results);
|
|
196
|
+
|
|
197
|
+
const failed = results.filter(r => !r.success).length;
|
|
198
|
+
if (failed > 0) {
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
await cleanupTempConfig();
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
|
|
@@ -18,19 +18,14 @@ const yaml = require('js-yaml');
|
|
|
18
18
|
const chalk = require('chalk');
|
|
19
19
|
const { getDeploymentAuth } = require('../../lib/utils/token-manager');
|
|
20
20
|
const { discoverDataplaneUrl } = require('../../lib/commands/wizard-dataplane');
|
|
21
|
+
const { resolveControllerUrl } = require('../../lib/utils/controller-url');
|
|
22
|
+
const { resolveEnvironment } = require('../../lib/core/config');
|
|
21
23
|
|
|
22
24
|
const execFileAsync = promisify(execFile);
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
const DEFAULT_ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
|
|
26
|
-
const DEFAULT_DATAPLANE_URL = process.env.DATAPLANE_URL || '';
|
|
27
|
-
const DEFAULT_OPENAPI_FILE = process.env.HUBSPOT_OPENAPI_FILE ||
|
|
28
|
-
'/workspace/aifabrix-dataplane/data/hubspot/openapi/companies.json';
|
|
29
|
-
const LOCAL_ENV_PATH = path.join(process.cwd(), 'integration', 'hubspot', '.env');
|
|
30
|
-
const DEFAULT_ENV_PATH = process.env.HUBSPOT_ENV_PATH ||
|
|
31
|
-
(fsSync.existsSync(LOCAL_ENV_PATH) ? LOCAL_ENV_PATH : '/workspace/aifabrix-dataplane/data/hubspot/.env');
|
|
32
|
-
|
|
26
|
+
/** Single source for test config: integration/hubspot/.env */
|
|
33
27
|
const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot');
|
|
28
|
+
const LOCAL_ENV_PATH = path.join(HUBSPOT_DIR, '.env');
|
|
34
29
|
const ARTIFACT_DIR = path.join(HUBSPOT_DIR, 'test-artifacts');
|
|
35
30
|
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
36
31
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -271,11 +266,54 @@ async function loadEnvFile(envPath, options) {
|
|
|
271
266
|
process.env[key] = value;
|
|
272
267
|
}
|
|
273
268
|
}
|
|
269
|
+
// Map common .env keys so tests and wizard use credentials from integration/hubspot/.env
|
|
270
|
+
if (process.env.CLIENTID && process.env.HUBSPOT_CLIENT_ID === undefined) {
|
|
271
|
+
process.env.HUBSPOT_CLIENT_ID = process.env.CLIENTID;
|
|
272
|
+
}
|
|
273
|
+
if (process.env.CLIENTSECRET && process.env.HUBSPOT_CLIENT_SECRET === undefined) {
|
|
274
|
+
process.env.HUBSPOT_CLIENT_SECRET = process.env.CLIENTSECRET;
|
|
275
|
+
}
|
|
274
276
|
if (options.verbose) {
|
|
275
277
|
logInfo(`Loaded env vars from: ${envPath}`);
|
|
276
278
|
}
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Load test config (controller, environment, dataplane, openapi file).
|
|
283
|
+
* Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
|
|
284
|
+
* the same resolution as the CLI (aifx auth status) so tests use the same controller.
|
|
285
|
+
* @async
|
|
286
|
+
* @function loadTestConfigFromEnv
|
|
287
|
+
* @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
|
|
288
|
+
*/
|
|
289
|
+
async function loadTestConfigFromEnv() {
|
|
290
|
+
const defaults = {
|
|
291
|
+
DATAPLANE_URL: '',
|
|
292
|
+
HUBSPOT_OPENAPI_FILE: path.join(HUBSPOT_DIR, 'companies.json')
|
|
293
|
+
};
|
|
294
|
+
let parsed = {};
|
|
295
|
+
try {
|
|
296
|
+
if (fsSync.existsSync(LOCAL_ENV_PATH)) {
|
|
297
|
+
const content = await fs.readFile(LOCAL_ENV_PATH, 'utf8');
|
|
298
|
+
parsed = parseEnvFile(content);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logWarn(`Could not read ${LOCAL_ENV_PATH}: ${error.message}`);
|
|
302
|
+
}
|
|
303
|
+
const controllerUrl = parsed.CONTROLLER_URL
|
|
304
|
+
? parsed.CONTROLLER_URL.trim()
|
|
305
|
+
: await resolveControllerUrl();
|
|
306
|
+
const environment = parsed.ENVIRONMENT
|
|
307
|
+
? parsed.ENVIRONMENT.trim()
|
|
308
|
+
: await resolveEnvironment();
|
|
309
|
+
return {
|
|
310
|
+
controllerUrl,
|
|
311
|
+
environment,
|
|
312
|
+
dataplaneUrl: parsed.DATAPLANE_URL || defaults.DATAPLANE_URL,
|
|
313
|
+
openapiFile: parsed.HUBSPOT_OPENAPI_FILE || defaults.HUBSPOT_OPENAPI_FILE
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
279
317
|
/**
|
|
280
318
|
* Ensure dataplane URL is available for tests
|
|
281
319
|
* @async
|
|
@@ -392,7 +430,8 @@ async function validateAuth(context, options) {
|
|
|
392
430
|
throw new Error('Authentication check failed. Run: node bin/aifabrix.js login --controller <url> --method device');
|
|
393
431
|
}
|
|
394
432
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
395
|
-
|
|
433
|
+
// Only treat "Not authenticated" as failure; "✗ Not reachable" (dataplane) is not auth failure
|
|
434
|
+
if (output.includes('Not authenticated')) {
|
|
396
435
|
throw new Error('Not authenticated. Run: node bin/aifabrix.js login --controller <url> --method device');
|
|
397
436
|
}
|
|
398
437
|
logSuccess('Authentication validated.');
|
|
@@ -1216,7 +1255,7 @@ async function corruptSystemFileWithInvalidRole(appPath) {
|
|
|
1216
1255
|
* @returns {Promise<void>} Resolves when file is corrupted
|
|
1217
1256
|
*/
|
|
1218
1257
|
async function corruptRbacFile(appPath) {
|
|
1219
|
-
const rbacPath = path.join(appPath, 'rbac.
|
|
1258
|
+
const rbacPath = path.join(appPath, 'rbac.yaml');
|
|
1220
1259
|
await fs.writeFile(rbacPath, 'invalid: yaml: syntax: [', 'utf8');
|
|
1221
1260
|
}
|
|
1222
1261
|
|
|
@@ -1339,7 +1378,7 @@ function buildNegativeRbacTestCases(context) {
|
|
|
1339
1378
|
const appName = 'hubspot-test-negative-rbac-invalid-yaml';
|
|
1340
1379
|
const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-yaml-test', context, options);
|
|
1341
1380
|
await corruptRbacFile(appPath);
|
|
1342
|
-
await runValidationExpectFailure(appName, context, options, '
|
|
1381
|
+
await runValidationExpectFailure(appName, context, options, 'Invalid YAML syntax in rbac.yaml');
|
|
1343
1382
|
await cleanupAppArtifacts(appName, options);
|
|
1344
1383
|
}
|
|
1345
1384
|
}
|
|
@@ -1485,13 +1524,8 @@ async function main() {
|
|
|
1485
1524
|
return;
|
|
1486
1525
|
}
|
|
1487
1526
|
await ensureDir(ARTIFACT_DIR);
|
|
1488
|
-
await loadEnvFile(
|
|
1489
|
-
const context =
|
|
1490
|
-
controllerUrl: DEFAULT_CONTROLLER_URL,
|
|
1491
|
-
environment: DEFAULT_ENVIRONMENT,
|
|
1492
|
-
dataplaneUrl: DEFAULT_DATAPLANE_URL,
|
|
1493
|
-
openapiFile: DEFAULT_OPENAPI_FILE
|
|
1494
|
-
};
|
|
1527
|
+
await loadEnvFile(LOCAL_ENV_PATH, args);
|
|
1528
|
+
const context = await loadTestConfigFromEnv();
|
|
1495
1529
|
setupTestContext(context);
|
|
1496
1530
|
await validateAuth(context, args);
|
|
1497
1531
|
const testCases = buildTestCases(context).filter(testCase => (
|
package/lib/app/config.js
CHANGED
|
@@ -66,7 +66,6 @@ function generateExternalSystemEnvTemplate(config, appName) {
|
|
|
66
66
|
lines.push('CLIENTID=kv://' + systemKey + '-clientidKeyVault');
|
|
67
67
|
lines.push('CLIENTSECRET=kv://' + systemKey + '-clientsecretKeyVault');
|
|
68
68
|
lines.push('TOKENURL=https://api.example.com/oauth/token');
|
|
69
|
-
lines.push('REDIRECT_URI=kv://' + systemKey + '-redirect-uriKeyVault');
|
|
70
69
|
} else if (authType === 'apikey') {
|
|
71
70
|
lines.push('API_KEY=kv://' + systemKey + '-api-keyKeyVault');
|
|
72
71
|
} else if (authType === 'basic') {
|
package/lib/app/show-display.js
CHANGED
|
@@ -51,7 +51,7 @@ function logApplicationExternalIntegration(ei) {
|
|
|
51
51
|
logger.log(` schemaBasePath: ${ei.schemaBasePath}`);
|
|
52
52
|
logger.log(` systems: [${(ei.systems || []).join(', ')}]`);
|
|
53
53
|
logger.log(` dataSources: [${(ei.dataSources || []).join(', ')}]`);
|
|
54
|
-
logger.log(chalk.gray('\n For external system data as on dataplane, run
|
|
54
|
+
logger.log(chalk.gray('\n For external system data as on dataplane, run: aifabrix show <appKey> --online or aifabrix app show <appKey>.'));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function logApplicationSection(a, isExternal) {
|
|
@@ -130,6 +130,31 @@ function logExternalSystemDataSources(dataSources) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Log OpenAPI and MCP documentation links for external system (dataplane endpoints).
|
|
135
|
+
* REST: /api/v1/rest/{systemKey}/docs; MCP: /api/v1/mcp/{systemKey}/{resourceType}/docs per dataSource.
|
|
136
|
+
* @param {Object} ext - External system result (dataplaneUrl, systemKey, dataSources, openapiFiles, openapiEndpoints)
|
|
137
|
+
*/
|
|
138
|
+
function logExternalSystemServiceLinks(ext) {
|
|
139
|
+
if (!ext || !ext.dataplaneUrl || !ext.systemKey) return;
|
|
140
|
+
const base = ext.dataplaneUrl.replace(/\/$/, '');
|
|
141
|
+
const sk = ext.systemKey;
|
|
142
|
+
const hasOpenApi = (ext.openapiFiles && ext.openapiFiles.length > 0) ||
|
|
143
|
+
(ext.openapiEndpoints && ext.openapiEndpoints.length > 0);
|
|
144
|
+
const dataSources = ext.dataSources || [];
|
|
145
|
+
const hasMcp = dataSources.length > 0;
|
|
146
|
+
if (!hasOpenApi && !hasMcp) return;
|
|
147
|
+
|
|
148
|
+
logger.log(' Service links:');
|
|
149
|
+
logger.log(` REST OpenAPI: ${base}/api/v1/rest/${sk}/docs`);
|
|
150
|
+
if (hasMcp) {
|
|
151
|
+
dataSources.forEach((ds) => {
|
|
152
|
+
const resourceType = ds.key || ds.systemKey || sk;
|
|
153
|
+
logger.log(` MCP ${resourceType}: ${base}/api/v1/mcp/${sk}/${resourceType}/docs`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
function logExternalSystemApplication(ap) {
|
|
134
159
|
if (!ap) return;
|
|
135
160
|
logger.log(' Application (from dataplane):');
|
|
@@ -155,6 +180,7 @@ function logExternalSystemSection(ext) {
|
|
|
155
180
|
const sample = ext.openapiEndpoints.slice(0, 3).map((e) => `${e.method || 'GET'} ${e.path || e.pathPattern || e}`).join(', ');
|
|
156
181
|
logger.log(` OpenAPI endpoints: ${ext.openapiEndpoints.length} (e.g. ${sample}${ext.openapiEndpoints.length > 3 ? ' …' : ''})`);
|
|
157
182
|
}
|
|
183
|
+
logExternalSystemServiceLinks(ext);
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
/**
|
package/lib/commands/app.js
CHANGED
|
@@ -14,6 +14,7 @@ const logger = require('../utils/logger');
|
|
|
14
14
|
const { listApplications } = require('../app/list');
|
|
15
15
|
const { registerApplication } = require('../app/register');
|
|
16
16
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
|
+
const { showApp } = require('../app/show');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Setup application management commands
|
|
@@ -66,6 +67,20 @@ function setupAppCommands(program) {
|
|
|
66
67
|
process.exit(1);
|
|
67
68
|
}
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
// Show command: show application from controller (online). Same as aifabrix show <appKey> --online.
|
|
72
|
+
app
|
|
73
|
+
.command('show <appKey>')
|
|
74
|
+
.description('Show application from controller (online). Same as aifabrix show <appKey> --online')
|
|
75
|
+
.option('--json', 'Output as JSON')
|
|
76
|
+
.action(async(appKey, options) => {
|
|
77
|
+
try {
|
|
78
|
+
await showApp(appKey, { online: true, json: !!options.json });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
module.exports = { setupAppCommands };
|
|
@@ -12,13 +12,15 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
/**
|
|
13
13
|
* Build preferences object for wizard.yaml (schema shape: intent, fieldOnboardingLevel, enableOpenAPIGeneration, enableMCP, enableABAC, enableRBAC)
|
|
14
14
|
* @param {string} intent - User intent
|
|
15
|
-
* @param {Object} preferences - From promptForUserPreferences (mcp, abac, rbac)
|
|
15
|
+
* @param {Object} preferences - From promptForUserPreferences (fieldOnboardingLevel, mcp, abac, rbac)
|
|
16
16
|
* @returns {Object} Preferences for wizard-config schema
|
|
17
17
|
*/
|
|
18
18
|
function buildPreferencesForSave(intent, preferences) {
|
|
19
|
+
const level = preferences?.fieldOnboardingLevel;
|
|
20
|
+
const validLevel = level === 'standard' || level === 'minimal' ? level : 'full';
|
|
19
21
|
return {
|
|
20
22
|
intent: intent || 'general integration',
|
|
21
|
-
fieldOnboardingLevel:
|
|
23
|
+
fieldOnboardingLevel: validLevel,
|
|
22
24
|
enableOpenAPIGeneration: true,
|
|
23
25
|
enableMCP: Boolean(preferences?.mcp),
|
|
24
26
|
enableABAC: Boolean(preferences?.abac),
|
|
@@ -78,11 +80,18 @@ function formatSourceLine(source) {
|
|
|
78
80
|
* @returns {string|null}
|
|
79
81
|
*/
|
|
80
82
|
function formatPreferencesLine(preferences) {
|
|
81
|
-
if (!preferences || (!preferences.intent && (preferences.enableMCP === undefined || preferences.enableMCP === null))) {
|
|
83
|
+
if (!preferences || (!preferences.intent && (preferences.enableMCP === undefined || preferences.enableMCP === null) && !preferences.fieldOnboardingLevel)) {
|
|
82
84
|
return null;
|
|
83
85
|
}
|
|
84
86
|
const p = preferences;
|
|
85
|
-
|
|
87
|
+
const parts = [
|
|
88
|
+
p.fieldOnboardingLevel && `level=${p.fieldOnboardingLevel}`,
|
|
89
|
+
p.intent && `intent=${p.intent}`,
|
|
90
|
+
p.enableMCP && 'MCP',
|
|
91
|
+
p.enableABAC && 'ABAC',
|
|
92
|
+
p.enableRBAC && 'RBAC'
|
|
93
|
+
].filter(Boolean);
|
|
94
|
+
return parts.length ? parts.join(', ') : '(defaults)';
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
package/lib/commands/wizard.js
CHANGED
|
@@ -148,6 +148,7 @@ async function handleInteractiveConfigGeneration(options) {
|
|
|
148
148
|
|
|
149
149
|
const configPrefs = {
|
|
150
150
|
intent: userIntent,
|
|
151
|
+
fieldOnboardingLevel: preferences.fieldOnboardingLevel || 'full',
|
|
151
152
|
enableMCP: preferences.mcp,
|
|
152
153
|
enableABAC: preferences.abac,
|
|
153
154
|
enableRBAC: preferences.rbac
|
|
@@ -309,10 +309,21 @@ async function promptForUserIntent() {
|
|
|
309
309
|
* Prompt for user preferences
|
|
310
310
|
* @async
|
|
311
311
|
* @function promptForUserPreferences
|
|
312
|
-
* @returns {Promise<Object>} User preferences object
|
|
312
|
+
* @returns {Promise<Object>} User preferences object (fieldOnboardingLevel, mcp, abac, rbac)
|
|
313
313
|
*/
|
|
314
314
|
async function promptForUserPreferences() {
|
|
315
315
|
const answers = await inquirer.prompt([
|
|
316
|
+
{
|
|
317
|
+
type: 'list',
|
|
318
|
+
name: 'fieldOnboardingLevel',
|
|
319
|
+
message: 'Field onboarding level:',
|
|
320
|
+
choices: [
|
|
321
|
+
{ name: 'full - All fields mapped and indexed', value: 'full' },
|
|
322
|
+
{ name: 'standard - Core and important fields only', value: 'standard' },
|
|
323
|
+
{ name: 'minimal - Essential fields only', value: 'minimal' }
|
|
324
|
+
],
|
|
325
|
+
default: 'full'
|
|
326
|
+
},
|
|
316
327
|
{
|
|
317
328
|
type: 'confirm',
|
|
318
329
|
name: 'mcp',
|
|
@@ -333,6 +344,7 @@ async function promptForUserPreferences() {
|
|
|
333
344
|
}
|
|
334
345
|
]);
|
|
335
346
|
return {
|
|
347
|
+
fieldOnboardingLevel: answers.fieldOnboardingLevel,
|
|
336
348
|
mcp: answers.mcp,
|
|
337
349
|
abac: answers.abac,
|
|
338
350
|
rbac: answers.rbac
|
|
@@ -20,7 +20,7 @@ environments:
|
|
|
20
20
|
KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
|
|
21
21
|
KEYCLOAK_PUBLIC_PORT: 8082
|
|
22
22
|
DATAPLANE_HOST: dataplane
|
|
23
|
-
DATAPLANE_PORT: 3001
|
|
23
|
+
DATAPLANE_PORT: 3001 # Internal port (container-to-container). DATAPLANE_PUBLIC_PORT calculated automatically.
|
|
24
24
|
NODE_ENV: production
|
|
25
25
|
PYTHONUNBUFFERED: 1
|
|
26
26
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -33,10 +33,8 @@ environments:
|
|
|
33
33
|
REDIS_PORT: 6379
|
|
34
34
|
MISO_HOST: localhost
|
|
35
35
|
MISO_PORT: 3010
|
|
36
|
-
MISO_PUBLIC_PORT: 3010
|
|
37
36
|
KEYCLOAK_HOST: localhost
|
|
38
37
|
KEYCLOAK_PORT: 8082
|
|
39
|
-
KEYCLOAK_PUBLIC_PORT: 8082
|
|
40
38
|
DATAPLANE_HOST: localhost
|
|
41
39
|
DATAPLANE_PORT: 3011
|
|
42
40
|
NODE_ENV: development
|
|
@@ -25,8 +25,9 @@ function getSecretsFilePath() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Load client credentials from secrets.local.yaml
|
|
29
|
-
* Reads using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault
|
|
28
|
+
* Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot/.env).
|
|
29
|
+
* Reads secrets file using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault.
|
|
30
|
+
* If not found, checks process.env.CLIENTID and process.env.CLIENTSECRET (set when .env is loaded).
|
|
30
31
|
* @param {string} appName - Application name
|
|
31
32
|
* @returns {Promise<{clientId: string, clientSecret: string}|null>} Credentials or null if not found
|
|
32
33
|
*/
|
|
@@ -37,31 +38,38 @@ async function loadClientCredentials(appName) {
|
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
40
|
const secretsFile = getSecretsFilePath();
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
if (fs.existsSync(secretsFile)) {
|
|
42
|
+
const content = fs.readFileSync(secretsFile, 'utf8');
|
|
43
|
+
const secrets = yaml.load(content) || {};
|
|
44
|
+
|
|
45
|
+
const clientIdKey = `${appName}-client-idKeyVault`;
|
|
46
|
+
const clientSecretKey = `${appName}-client-secretKeyVault`;
|
|
47
|
+
|
|
48
|
+
const clientId = secrets[clientIdKey];
|
|
49
|
+
const clientSecret = secrets[clientSecretKey];
|
|
50
|
+
|
|
51
|
+
if (clientId && clientSecret) {
|
|
52
|
+
return {
|
|
53
|
+
clientId: String(clientId),
|
|
54
|
+
clientSecret: String(clientSecret)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
55
57
|
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
60
|
+
}
|
|
56
61
|
|
|
62
|
+
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot/.env)
|
|
63
|
+
const envClientId = process.env.CLIENTID || process.env.CLIENT_ID;
|
|
64
|
+
const envClientSecret = process.env.CLIENTSECRET || process.env.CLIENT_SECRET;
|
|
65
|
+
if (envClientId && envClientSecret) {
|
|
57
66
|
return {
|
|
58
|
-
clientId:
|
|
59
|
-
clientSecret:
|
|
67
|
+
clientId: String(envClientId).trim(),
|
|
68
|
+
clientSecret: String(envClientSecret).trim()
|
|
60
69
|
};
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
63
|
-
return null;
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
package/package.json
CHANGED