@aifabrix/builder 2.44.4 → 2.44.5
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/jest.projects.js +20 -15
- package/lib/api/types/wizard.types.js +2 -1
- package/lib/cli/setup-app.help.js +1 -1
- package/lib/cli/setup-app.test-commands.js +9 -5
- package/lib/cli/setup-infra.js +6 -2
- package/lib/cli/setup-utility.js +19 -0
- package/lib/commands/repair-rbac.js +25 -2
- package/lib/commands/repair.js +1 -17
- package/lib/commands/test-e2e-external.js +4 -3
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/wizard-core.js +53 -11
- package/lib/commands/wizard-entity-selection.js +71 -14
- package/lib/commands/wizard-headless.js +5 -2
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +208 -60
- package/lib/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/external-readme.js +47 -3
- package/lib/utils/urls-local-registry.js +52 -10
- package/package.json +1 -1
- package/templates/applications/miso-controller/env.template +6 -6
- package/templates/external-system/README.md.hbs +58 -31
package/jest.projects.js
CHANGED
|
@@ -57,24 +57,28 @@ const defaultProject = {
|
|
|
57
57
|
'/tests/lib/utils/cli-utils.test.js',
|
|
58
58
|
'/tests/lib/utils/external-system-display.test.js',
|
|
59
59
|
'/tests/lib/utils/dev-hosts-helper.test.js',
|
|
60
|
-
'/tests/lib/utils/
|
|
60
|
+
'/tests/lib/utils/register-aifabrix-shell-env.test.js',
|
|
61
61
|
'/tests/lib/utils/datasource-validation-watch.test.js',
|
|
62
62
|
'\\\\tests\\\\lib\\\\utils\\\\cli-utils.test.js',
|
|
63
63
|
'\\\\tests\\\\lib\\\\utils\\\\external-system-display.test.js',
|
|
64
64
|
'\\\\tests\\\\lib\\\\utils\\\\dev-hosts-helper.test.js',
|
|
65
|
-
'\\\\tests\\\\lib\\\\utils\\\\
|
|
65
|
+
'\\\\tests\\\\lib\\\\utils\\\\register-aifabrix-shell-env.test.js',
|
|
66
66
|
'\\\\tests\\\\lib\\\\utils\\\\datasource-validation-watch.test.js',
|
|
67
67
|
'lib/utils/dev-hosts-helper.test.js',
|
|
68
|
-
'lib/utils/
|
|
68
|
+
'lib/utils/register-aifabrix-shell-env.test.js',
|
|
69
69
|
'lib/utils/datasource-validation-watch.test.js',
|
|
70
70
|
'dev-hosts-helper\\.test\\.js',
|
|
71
|
-
'
|
|
71
|
+
'register-aifabrix-shell-env\\.test\\.js',
|
|
72
72
|
'/tests/lib/datasource/log-viewer.test.js',
|
|
73
73
|
'\\\\tests\\\\lib\\\\datasource\\\\log-viewer.test.js',
|
|
74
74
|
'lib/datasource/log-viewer.test.js',
|
|
75
75
|
'/tests/lib/datasource/log-viewer-structural.test.js',
|
|
76
|
+
'/tests/lib/datasource/log-viewer-run.test.js',
|
|
76
77
|
'\\\\tests\\\\lib\\\\datasource\\\\log-viewer-structural.test.js',
|
|
78
|
+
'\\\\tests\\\\lib\\\\datasource\\\\log-viewer-run.test.js',
|
|
77
79
|
'lib/datasource/log-viewer-structural.test.js',
|
|
80
|
+
'lib/datasource/log-viewer-run.test.js',
|
|
81
|
+
'log-viewer-run\\.test\\.js',
|
|
78
82
|
'/tests/lib/commands/parameters-validate.test.js',
|
|
79
83
|
'\\\\tests\\\\lib\\\\commands\\\\parameters-validate.test.js',
|
|
80
84
|
'lib/commands/parameters-validate.test.js',
|
|
@@ -153,10 +157,6 @@ const defaultProject = {
|
|
|
153
157
|
'\\\\tests\\\\lib\\\\utils\\\\paths-app-listing.test.js',
|
|
154
158
|
'lib/utils/paths-app-listing.test.js',
|
|
155
159
|
'paths-app-listing\\.test\\.js',
|
|
156
|
-
'/tests/lib/utils/url-declarative-truth-table-124.test.js',
|
|
157
|
-
'\\\\tests\\\\lib\\\\utils\\\\url-declarative-truth-table-124.test.js',
|
|
158
|
-
'lib/utils/url-declarative-truth-table-124.test.js',
|
|
159
|
-
'url-declarative-truth-table-124\\.test\\.js',
|
|
160
160
|
'/tests/lib/generator/generator-external-rbac.test.js',
|
|
161
161
|
'\\\\tests\\\\lib\\\\generator\\\\generator-external-rbac.test.js',
|
|
162
162
|
'lib/generator/generator-external-rbac.test.js',
|
|
@@ -200,6 +200,10 @@ const defaultProject = {
|
|
|
200
200
|
'/tests/lib/app/app.test.js',
|
|
201
201
|
'\\\\tests\\\\lib\\\\app\\\\app.test.js',
|
|
202
202
|
'lib/app/app.test.js',
|
|
203
|
+
'/tests/lib/templates/application-frontdoor-paths.contract.test.js',
|
|
204
|
+
'\\\\tests\\\\lib\\\\templates\\\\application-frontdoor-paths.contract.test.js',
|
|
205
|
+
'lib/templates/application-frontdoor-paths.contract.test.js',
|
|
206
|
+
'application-frontdoor-paths\\.contract\\.test\\.js',
|
|
203
207
|
'/tests/lib/core/admin-secrets.test.js',
|
|
204
208
|
'\\\\tests\\\\lib\\\\core\\\\admin-secrets.test.js',
|
|
205
209
|
'lib/core/admin-secrets.test.js'
|
|
@@ -221,9 +225,6 @@ const isolatedProjects = [
|
|
|
221
225
|
makeIsolatedProject('cli-utils', ['**/tests/lib/utils/cli-utils.test.js']),
|
|
222
226
|
makeIsolatedProject('external-system-display', ['**/tests/lib/utils/external-system-display.test.js']),
|
|
223
227
|
makeIsolatedProject('dev-hosts-helper', ['**/tests/lib/utils/dev-hosts-helper.test.js']),
|
|
224
|
-
makeIsolatedProject('declarative-url-matrix-d-reload', [
|
|
225
|
-
'**/tests/lib/utils/declarative-url-matrix-d-reload.test.js'
|
|
226
|
-
]),
|
|
227
228
|
makeIsolatedProject('parameters-validate', ['**/tests/lib/commands/parameters-validate.test.js']),
|
|
228
229
|
makeIsolatedProject('paths-app-listing', ['**/tests/lib/utils/paths-app-listing.test.js']),
|
|
229
230
|
makeIsolatedProject('datasource-validation-watch', [
|
|
@@ -231,7 +232,11 @@ const isolatedProjects = [
|
|
|
231
232
|
]),
|
|
232
233
|
makeIsolatedProject('log-viewer', [
|
|
233
234
|
'**/tests/lib/datasource/log-viewer.test.js',
|
|
234
|
-
'**/tests/lib/datasource/log-viewer-structural.test.js'
|
|
235
|
+
'**/tests/lib/datasource/log-viewer-structural.test.js',
|
|
236
|
+
'**/tests/lib/datasource/log-viewer-run.test.js'
|
|
237
|
+
]),
|
|
238
|
+
makeIsolatedProject('register-aifabrix-shell-env', [
|
|
239
|
+
'**/tests/lib/utils/register-aifabrix-shell-env.test.js'
|
|
235
240
|
]),
|
|
236
241
|
makeIsolatedProject('datasource-test-run-schema-sync', [
|
|
237
242
|
'**/tests/lib/utils/datasource-test-run-schema-sync.test.js'
|
|
@@ -239,6 +244,9 @@ const isolatedProjects = [
|
|
|
239
244
|
makeIsolatedProject('infra-platform-contract', [
|
|
240
245
|
'**/tests/lib/parameters/infra-platform-contract.test.js'
|
|
241
246
|
]),
|
|
247
|
+
makeIsolatedProject('application-frontdoor-paths-contract', [
|
|
248
|
+
'**/tests/lib/templates/application-frontdoor-paths.contract.test.js'
|
|
249
|
+
]),
|
|
242
250
|
makeIsolatedProject('database-secret-values', [
|
|
243
251
|
'**/tests/lib/parameters/database-secret-values.test.js'
|
|
244
252
|
]),
|
|
@@ -283,9 +291,6 @@ const isolatedProjects = [
|
|
|
283
291
|
makeIsolatedProject('helpers-ensure-admin-secrets', [
|
|
284
292
|
'**/tests/lib/infrastructure/helpers-ensure-admin-secrets.test.js'
|
|
285
293
|
]),
|
|
286
|
-
makeIsolatedProject('url-declarative-truth-table-124', [
|
|
287
|
-
'**/tests/lib/utils/url-declarative-truth-table-124.test.js'
|
|
288
|
-
]),
|
|
289
294
|
makeIsolatedProject('secrets-generator', ['**/tests/lib/utils/secrets-generator.test.js']),
|
|
290
295
|
makeIsolatedProject('app-uncovered-lines', ['**/tests/lib/app/app-uncovered-lines.test.js']),
|
|
291
296
|
makeIsolatedProject('ensure-dev-certs-for-remote-docker', [
|
|
@@ -260,12 +260,13 @@
|
|
|
260
260
|
* @property {string} [source.serverUrl] - MCP server URL (for mcp-server)
|
|
261
261
|
* @property {string} [source.token] - MCP token (for mcp-server, supports ${ENV_VAR})
|
|
262
262
|
* @property {string} [source.platform] - Known platform (for known-platform)
|
|
263
|
+
* @property {string} [source.entityName] - Entity from discover-entities (openapi-file / openapi-url); skips interactive Step 4.5 when valid
|
|
263
264
|
* @property {Object} [credential] - Credential configuration
|
|
264
265
|
* @property {string} credential.action - Action ('create' | 'select' | 'skip')
|
|
265
266
|
* @property {string} [credential.credentialIdOrKey] - Credential ID/key (for select)
|
|
266
267
|
* @property {Object} [credential.config] - Credential config (for create)
|
|
267
268
|
* @property {Object} [preferences] - Generation preferences
|
|
268
|
-
* @property {string} [preferences.intent] - User intent (any descriptive text)
|
|
269
|
+
* @property {string} [preferences.intent] - User intent (any descriptive text, max 1000 chars)
|
|
269
270
|
* @property {string} [preferences.fieldOnboardingLevel] - Field level ('full' | 'standard' | 'minimal')
|
|
270
271
|
* @property {boolean} [preferences.enableOpenAPIGeneration] - Enable OpenAPI generation
|
|
271
272
|
* @property {boolean} [preferences.enableMCP] - Enable MCP
|
|
@@ -56,7 +56,7 @@ Examples:
|
|
|
56
56
|
Notes:
|
|
57
57
|
- To run E2E for one datasource, use:
|
|
58
58
|
aifabrix datasource test-e2e <datasourceKey>
|
|
59
|
-
-
|
|
59
|
+
- External integration: local system and datasource files are published to the dataplane before E2E (same as upload). Use --no-sync to exercise only config already deployed. The old --sync flag is a no-op (kept for scripts).
|
|
60
60
|
`;
|
|
61
61
|
|
|
62
62
|
module.exports = {
|
|
@@ -91,7 +91,7 @@ async function runExternalIntegrationE2EAndCertSync(appName, options) {
|
|
|
91
91
|
debug: options.debug,
|
|
92
92
|
verbose: options.verbose,
|
|
93
93
|
async: options.async !== false,
|
|
94
|
-
|
|
94
|
+
noSync: options.noSync === true
|
|
95
95
|
});
|
|
96
96
|
const { displayIntegrationTestResults } = require('../utils/external-system-display');
|
|
97
97
|
const datasourceResults = results.map(r => ({
|
|
@@ -121,10 +121,10 @@ async function runExternalIntegrationE2EAndCertSync(appName, options) {
|
|
|
121
121
|
async function runTestE2ECommand(appName, options) {
|
|
122
122
|
const pathsUtil = require('../utils/paths');
|
|
123
123
|
const appType = await pathsUtil.detectAppType(appName).catch(() => null);
|
|
124
|
-
if (options.
|
|
124
|
+
if (options.noSync === true && appType && appType.baseDir === 'builder') {
|
|
125
125
|
throw new Error(
|
|
126
|
-
'Option --sync applies only to external integration E2E (integration/<systemKey>/). ' +
|
|
127
|
-
'Remove --sync for builder app E2E
|
|
126
|
+
'Option --no-sync applies only to external integration E2E (integration/<systemKey>/). ' +
|
|
127
|
+
'Remove --no-sync for builder app E2E.'
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
if (appType && appType.baseDir === 'integration') {
|
|
@@ -166,7 +166,11 @@ function setupTestE2eCommand(program) {
|
|
|
166
166
|
.option('-d, --debug', 'Include debug output and write log to integration/<systemKey>/logs/')
|
|
167
167
|
.option(
|
|
168
168
|
'--sync',
|
|
169
|
-
'
|
|
169
|
+
'(Deprecated; no-op.) Local integration files are published before E2E by default; use --no-sync to skip.'
|
|
170
|
+
)
|
|
171
|
+
.option(
|
|
172
|
+
'--no-sync',
|
|
173
|
+
'Skip publishing local integration files; E2E uses the system config already on the dataplane (external integration only)'
|
|
170
174
|
)
|
|
171
175
|
.option('--warnings-as-errors', 'Treat aggregate warn as failure (exit 1)')
|
|
172
176
|
.option('--require-cert', 'Require certification passed on every datasource (exit 2 if not)')
|
package/lib/cli/setup-infra.js
CHANGED
|
@@ -18,7 +18,7 @@ const { resolveControllerUrl } = require('../utils/controller-url');
|
|
|
18
18
|
const { handleLogin } = require('../commands/login');
|
|
19
19
|
const { handleUpMiso } = require('../commands/up-miso');
|
|
20
20
|
const { handleUpDataplane } = require('../commands/up-dataplane');
|
|
21
|
-
const { cleanBuilderAppDirs } = require('../commands/up-common');
|
|
21
|
+
const { applyUpPlatformForceConfig, cleanBuilderAppDirs } = require('../commands/up-common');
|
|
22
22
|
const {
|
|
23
23
|
loadInfraStatusSummary,
|
|
24
24
|
formatInfraStatusTitleLine,
|
|
@@ -184,10 +184,14 @@ function setupUpPlatformCommand(program) {
|
|
|
184
184
|
.option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
|
|
185
185
|
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
186
186
|
.option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
|
|
187
|
-
.option(
|
|
187
|
+
.option(
|
|
188
|
+
'-f, --force',
|
|
189
|
+
'Reset CLI auth (clear all device/client tokens, set environment to dev, set default controller URL from developer-id), clean builder/keycloak, builder/miso-controller, builder/dataplane, then re-fetch from templates'
|
|
190
|
+
)
|
|
188
191
|
.action(async(options) => {
|
|
189
192
|
try {
|
|
190
193
|
if (options.force) {
|
|
194
|
+
await applyUpPlatformForceConfig();
|
|
191
195
|
await cleanBuilderAppDirs(['keycloak', 'miso-controller', 'dataplane']);
|
|
192
196
|
}
|
|
193
197
|
await handleUpMiso(options);
|
package/lib/cli/setup-utility.js
CHANGED
|
@@ -30,6 +30,24 @@ Examples:
|
|
|
30
30
|
$ aifabrix validate --builder
|
|
31
31
|
`;
|
|
32
32
|
|
|
33
|
+
const REPAIR_HELP_AFTER = `
|
|
34
|
+
Examples:
|
|
35
|
+
$ aifabrix repair hubspot-demo
|
|
36
|
+
Align manifest, system/datasource files, RBAC extract, env.template, and deploy JSON under integration/<systemKey>/.
|
|
37
|
+
|
|
38
|
+
$ aifabrix repair hubspot-demo --dry-run
|
|
39
|
+
Show what would change without writing files.
|
|
40
|
+
|
|
41
|
+
$ aifabrix repair hubspot-demo --auth oauth2
|
|
42
|
+
Set authentication method; updates the system file and env.template (oauth2, aad, apikey, basic, …).
|
|
43
|
+
|
|
44
|
+
$ aifabrix repair hubspot-demo --rbac --expose --sync --test
|
|
45
|
+
Optional datasource fixes: RBAC roles/permissions, exposed.schema from attributes, default sync block, testPayload stubs.
|
|
46
|
+
|
|
47
|
+
$ aifabrix repair hubspot-demo --doc
|
|
48
|
+
Regenerate README.md from the current deployment manifest only (other drift fixes unchanged).
|
|
49
|
+
`;
|
|
50
|
+
|
|
33
51
|
/**
|
|
34
52
|
* Resolve app path and type for split-json (integration first, then builder).
|
|
35
53
|
*
|
|
@@ -185,6 +203,7 @@ function setupRepairCommand(program) {
|
|
|
185
203
|
.option('--expose', 'Set exposed.schema on each datasource from all fieldMappings.attributes keys (metadata.<key>); removes deprecated exposed.attributes if present')
|
|
186
204
|
.option('--sync', 'Add default sync section to datasources that lack it')
|
|
187
205
|
.option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
|
|
206
|
+
.addHelpText('after', REPAIR_HELP_AFTER)
|
|
188
207
|
.action(async(appName, options) => {
|
|
189
208
|
try {
|
|
190
209
|
const { repairExternalIntegration } = require('../commands/repair');
|
|
@@ -17,6 +17,22 @@ const { resolveRbacPath } = require('../utils/app-config-resolver');
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
|
|
22
|
+
* @param {Object} system - Parsed system config
|
|
23
|
+
* @returns {Object|null} RBAC object or null
|
|
24
|
+
*/
|
|
25
|
+
function extractRbacFromSystem(system) {
|
|
26
|
+
if (!system || typeof system !== 'object') return null;
|
|
27
|
+
const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
|
|
28
|
+
const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
|
|
29
|
+
if (!hasRoles && !hasPermissions) return null;
|
|
30
|
+
const rbac = {};
|
|
31
|
+
if (hasRoles) rbac.roles = system.roles;
|
|
32
|
+
if (hasPermissions) rbac.permissions = system.permissions;
|
|
33
|
+
return rbac;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* Resolves capabilities from datasource (array or legacy object).
|
|
22
38
|
* @param {Object} parsed - Parsed datasource
|
|
@@ -24,9 +40,15 @@ const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
|
|
|
24
40
|
*/
|
|
25
41
|
function getCapabilitiesFromDatasource(parsed) {
|
|
26
42
|
const cap = parsed?.capabilities;
|
|
27
|
-
if (Array.isArray(cap))
|
|
43
|
+
if (Array.isArray(cap)) {
|
|
44
|
+
const arr = cap.filter(c => typeof c === 'string');
|
|
45
|
+
if (arr.length > 0) return arr;
|
|
46
|
+
return [...DEFAULT_CAPABILITIES];
|
|
47
|
+
}
|
|
28
48
|
if (cap && typeof cap === 'object') {
|
|
29
|
-
|
|
49
|
+
const keys = Object.keys(cap).filter(k => cap[k] === true);
|
|
50
|
+
if (keys.length > 0) return keys;
|
|
51
|
+
return [...DEFAULT_CAPABILITIES];
|
|
30
52
|
}
|
|
31
53
|
return [...DEFAULT_CAPABILITIES];
|
|
32
54
|
}
|
|
@@ -159,6 +181,7 @@ function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extrac
|
|
|
159
181
|
}
|
|
160
182
|
|
|
161
183
|
module.exports = {
|
|
184
|
+
extractRbacFromSystem,
|
|
162
185
|
getCapabilitiesFromDatasource,
|
|
163
186
|
mergeRbacFromDatasources
|
|
164
187
|
};
|
package/lib/commands/repair.js
CHANGED
|
@@ -26,7 +26,7 @@ const generator = require('../generator');
|
|
|
26
26
|
const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
|
|
27
27
|
const { generateReadmeFromDeployJson } = require('../generator/split-readme');
|
|
28
28
|
const { repairDatasourceFile } = require('./repair-datasource');
|
|
29
|
-
const { mergeRbacFromDatasources } = require('./repair-rbac');
|
|
29
|
+
const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
|
|
30
30
|
const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
|
|
31
31
|
const { normalizeDatasourceKeysAndFilenames } = require('./repair-datasource-keys');
|
|
32
32
|
|
|
@@ -49,22 +49,6 @@ function inferExternalReadmeFileExt(appPath) {
|
|
|
49
49
|
return '.json';
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Extracts roles and permissions from system object for rbac.yaml
|
|
54
|
-
* @param {Object} system - Parsed system config
|
|
55
|
-
* @returns {Object|null} RBAC object or null
|
|
56
|
-
*/
|
|
57
|
-
function extractRbacFromSystem(system) {
|
|
58
|
-
if (!system || typeof system !== 'object') return null;
|
|
59
|
-
const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
|
|
60
|
-
const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
|
|
61
|
-
if (!hasRoles && !hasPermissions) return null;
|
|
62
|
-
const rbac = {};
|
|
63
|
-
if (hasRoles) rbac.roles = system.roles;
|
|
64
|
-
if (hasPermissions) rbac.permissions = system.permissions;
|
|
65
|
-
return rbac;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
52
|
/**
|
|
69
53
|
* Loads first system file and returns parsed object with key
|
|
70
54
|
* @param {string} appPath - Application path
|
|
@@ -85,13 +85,14 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
88
|
+
* Publish local integration files to the dataplane before E2E (same path as `aifabrix upload <systemKey>`),
|
|
89
|
+
* unless ``options.noSync`` is true.
|
|
89
90
|
* @param {string} systemKey
|
|
90
91
|
* @param {Object} options
|
|
91
92
|
* @returns {Promise<void>}
|
|
92
93
|
*/
|
|
93
94
|
async function syncLocalIfRequested(systemKey, options) {
|
|
94
|
-
if (options.
|
|
95
|
+
if (options.noSync === true) return;
|
|
95
96
|
logger.log(chalk.cyan('Syncing local config to dataplane…'));
|
|
96
97
|
const { uploadExternalSystem } = require('./upload');
|
|
97
98
|
await uploadExternalSystem(systemKey, {
|
|
@@ -112,7 +113,7 @@ async function syncLocalIfRequested(systemKey, options) {
|
|
|
112
113
|
* @param {boolean} [options.debug] - Include debug, write log
|
|
113
114
|
* @param {boolean} [options.verbose] - Verbose output
|
|
114
115
|
* @param {boolean} [options.async] - If false, sync mode (default true)
|
|
115
|
-
* @param {boolean} [options.
|
|
116
|
+
* @param {boolean} [options.noSync] - When true, skip upload (E2E uses dataplane config already deployed)
|
|
116
117
|
* @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
|
|
117
118
|
*/
|
|
118
119
|
async function runTestE2EForExternalSystem(externalSystem, options = {}) {
|
|
@@ -13,6 +13,8 @@ const path = require('path');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const chalk = require('chalk');
|
|
15
15
|
const logger = require('../utils/logger');
|
|
16
|
+
const config = require('../core/config');
|
|
17
|
+
const { getDefaultControllerUrl } = require('../utils/controller-url');
|
|
16
18
|
const pathsUtil = require('../utils/paths');
|
|
17
19
|
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
18
20
|
const { isYamlPath } = require('../utils/config-format');
|
|
@@ -166,6 +168,28 @@ function patchEnvOutputPathForDeployOnly(appName) {
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/**
|
|
172
|
+
* For ``up-platform --force`` only: clear all stored auth tokens, set ``environment`` to ``dev``,
|
|
173
|
+
* set ``controller`` to the default URL for the current ``developer-id`` (``http://localhost`` +
|
|
174
|
+
* app port = 3000 + developerId × 100), and persist config. Does not change ``developer-id``.
|
|
175
|
+
* Builder dirs (keycloak, miso-controller, dataplane) are cleaned separately.
|
|
176
|
+
*
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*/
|
|
179
|
+
async function applyUpPlatformForceConfig() {
|
|
180
|
+
const deviceCleared = await config.clearAllDeviceTokens();
|
|
181
|
+
const clientCleared = await config.clearAllClientTokens();
|
|
182
|
+
await config.setCurrentEnvironment('dev');
|
|
183
|
+
const defaultControllerUrl = await getDefaultControllerUrl();
|
|
184
|
+
await config.setControllerUrl(defaultControllerUrl);
|
|
185
|
+
logger.log(
|
|
186
|
+
chalk.blue(
|
|
187
|
+
`--force: cleared ${deviceCleared} device token(s) and ${clientCleared} client token(s); ` +
|
|
188
|
+
`environment set to dev; default controller set to ${defaultControllerUrl} (run aifabrix login after platform is up)`
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
/**
|
|
170
194
|
* Removes builder app directories for the given app names. Only removes paths under the builder root
|
|
171
195
|
* to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
|
|
@@ -231,6 +255,7 @@ async function ensureAppFromTemplate(appName) {
|
|
|
231
255
|
}
|
|
232
256
|
|
|
233
257
|
module.exports = {
|
|
258
|
+
applyUpPlatformForceConfig,
|
|
234
259
|
cleanBuilderAppDirs,
|
|
235
260
|
ensureAppFromTemplate,
|
|
236
261
|
patchEnvOutputPathForDeployOnly,
|
|
@@ -275,7 +275,8 @@ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
|
|
|
275
275
|
* @param {string} [options.systemIdOrKey] - System ID or key (optional)
|
|
276
276
|
* @param {string} [options.sourceType] - Source type (use 'known-platform' to call platforms config endpoint)
|
|
277
277
|
* @param {string} [options.platformKey] - Platform key for known-platform (e.g. 'hubspot')
|
|
278
|
-
* @param {string} [options.appName] -
|
|
278
|
+
* @param {string} [options.appName] - Integration app key; for create-system, used as systemIdOrKey
|
|
279
|
+
* when not set so kv paths match the folder (avoids spec-title keys like "companies")
|
|
279
280
|
* @returns {Promise<Object>} Generated configuration
|
|
280
281
|
*/
|
|
281
282
|
async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
|
|
@@ -291,13 +292,19 @@ async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
|
|
|
291
292
|
});
|
|
292
293
|
return await getPlatformConfig(dataplaneUrl, authConfig, options.platformKey, platformPayload);
|
|
293
294
|
}
|
|
295
|
+
// Headless/interactive create-system uses appName as integration folder key; OpenAPI title alone
|
|
296
|
+
// can yield a wrong system key (e.g. title "Companies" → "companies"). Prefer appName when set.
|
|
297
|
+
let systemIdOrKeyForPayload = options.systemIdOrKey;
|
|
298
|
+
if (options.mode === 'create-system' && options.appName) {
|
|
299
|
+
systemIdOrKeyForPayload = systemIdOrKeyForPayload || options.appName;
|
|
300
|
+
}
|
|
294
301
|
const configPayload = buildConfigPayload({
|
|
295
302
|
openapiSpec: options.openapiSpec,
|
|
296
303
|
detectedType: options.detectedType,
|
|
297
304
|
mode: options.mode,
|
|
298
305
|
prefs,
|
|
299
306
|
credentialIdOrKey: options.credentialIdOrKey,
|
|
300
|
-
systemIdOrKey:
|
|
307
|
+
systemIdOrKey: systemIdOrKeyForPayload,
|
|
301
308
|
entityName: options.entityName,
|
|
302
309
|
systemDisplayName: options.systemDisplayName
|
|
303
310
|
});
|
|
@@ -410,6 +417,39 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
|
|
|
410
417
|
}
|
|
411
418
|
}
|
|
412
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
|
|
422
|
+
* @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
|
|
423
|
+
* @param {string} format - Project format: yaml | json
|
|
424
|
+
*/
|
|
425
|
+
function logWizardFileSaveFooter(appName, generatedFiles) {
|
|
426
|
+
logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
|
|
427
|
+
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
428
|
+
logger.log(chalk.blue('\nNext steps:'));
|
|
429
|
+
logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
|
|
430
|
+
logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
|
|
431
|
+
logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
|
|
435
|
+
const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
|
|
436
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
437
|
+
const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
|
|
438
|
+
const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
|
|
439
|
+
const rbacChanges = [];
|
|
440
|
+
const rbacUpdated = mergeRbacFromDatasources(
|
|
441
|
+
generatedFiles.appPath,
|
|
442
|
+
systemParsedForRbac,
|
|
443
|
+
datasourceFileNames,
|
|
444
|
+
extractRbacFromSystem,
|
|
445
|
+
{ format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
|
|
446
|
+
);
|
|
447
|
+
if (rbacUpdated && rbacChanges.length) {
|
|
448
|
+
rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
|
|
449
|
+
logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
413
453
|
/**
|
|
414
454
|
* Handle file saving step
|
|
415
455
|
* @async
|
|
@@ -418,17 +458,24 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
|
|
|
418
458
|
* @param {Object} systemConfig - System configuration
|
|
419
459
|
* @param {Object[]} datasourceConfigs - Datasource configurations
|
|
420
460
|
* @param {string} systemKey - System key
|
|
421
|
-
* @param {string}
|
|
422
|
-
* @param {Object} authConfig - Authentication configuration
|
|
461
|
+
* @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
|
|
423
462
|
* @returns {Promise<Object>} Generated files information
|
|
424
463
|
*/
|
|
425
|
-
async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey,
|
|
464
|
+
async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
|
|
465
|
+
const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
|
|
426
466
|
logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
|
|
427
467
|
const spinner = ora('Saving files...').start();
|
|
428
468
|
try {
|
|
429
469
|
const config = require('../core/config');
|
|
430
470
|
const format = (await config.getFormat()) || 'yaml';
|
|
431
471
|
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
|
|
472
|
+
if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
|
|
473
|
+
try {
|
|
474
|
+
mergeRbacAfterWizardFilesWritten(generatedFiles, format);
|
|
475
|
+
} catch (e) {
|
|
476
|
+
logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
432
479
|
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
433
480
|
try {
|
|
434
481
|
await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
|
|
@@ -437,12 +484,7 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
437
484
|
}
|
|
438
485
|
}
|
|
439
486
|
spinner.stop();
|
|
440
|
-
|
|
441
|
-
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
442
|
-
logger.log(chalk.blue('\nNext steps:'));
|
|
443
|
-
logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
|
|
444
|
-
logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
|
|
445
|
-
logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
|
|
487
|
+
logWizardFileSaveFooter(appName, generatedFiles);
|
|
446
488
|
return generatedFiles;
|
|
447
489
|
} catch (error) {
|
|
448
490
|
spinner.stop();
|
|
@@ -10,30 +10,87 @@ const { discoverEntities } = require('../api/wizard.api');
|
|
|
10
10
|
const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
|
|
11
11
|
const { promptForEntitySelection } = require('../generator/wizard-prompts');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* If wizard.yaml entity name matches discover-entities list, use it; else warn.
|
|
15
|
+
* @param {string} trimmed - Trimmed entity name from prefill
|
|
16
|
+
* @param {Array<{name: string}>} entities - Discovered entities
|
|
17
|
+
* @returns {string|null} Resolved name or null to prompt
|
|
18
|
+
*/
|
|
19
|
+
function resolvePrefillEntityName(trimmed, entities) {
|
|
20
|
+
const prefillCheck = validateEntityNameForOpenApi(trimmed, entities);
|
|
21
|
+
if (prefillCheck.valid) {
|
|
22
|
+
logger.log(chalk.gray(
|
|
23
|
+
`Using entity from wizard.yaml (${trimmed}). Skipping entity prompts.`
|
|
24
|
+
));
|
|
25
|
+
logger.log(chalk.green(`\u2713 Selected entity: ${trimmed}`));
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
logger.log(chalk.yellow(
|
|
29
|
+
`Warning: wizard.yaml source.entityName '${trimmed}' is not in the discover-entities list; choose manually.`
|
|
30
|
+
));
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prompt for entity and validate against list.
|
|
36
|
+
* @param {Array<{name: string}>} entities - Discovered entities
|
|
37
|
+
* @returns {Promise<string>} Valid entity name
|
|
38
|
+
*/
|
|
39
|
+
async function promptForValidatedEntity(entities) {
|
|
40
|
+
const entityName = await promptForEntitySelection(entities);
|
|
41
|
+
const validation = validateEntityNameForOpenApi(entityName, entities);
|
|
42
|
+
if (!validation.valid) {
|
|
43
|
+
throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
|
|
46
|
+
return entityName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Discover entities and select one (single-entity shortcut, prefill, or prompt).
|
|
51
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
52
|
+
* @param {Object} authConfig - Authentication configuration
|
|
53
|
+
* @param {Object} openapiSpec - OpenAPI specification
|
|
54
|
+
* @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
|
|
55
|
+
* @returns {Promise<string|null>} Selected entity name or null (skip)
|
|
56
|
+
*/
|
|
57
|
+
async function discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
|
|
58
|
+
const response = await discoverEntities(dataplaneUrl, authConfig, openapiSpec);
|
|
59
|
+
const entities = response?.data?.entities;
|
|
60
|
+
if (!Array.isArray(entities) || entities.length === 0) return null;
|
|
61
|
+
|
|
62
|
+
logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
|
|
63
|
+
|
|
64
|
+
if (entities.length === 1) {
|
|
65
|
+
const only = entities[0].name;
|
|
66
|
+
logger.log(chalk.green(`\u2713 Only one entity discovered; using: ${only}`));
|
|
67
|
+
return only;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trimmed =
|
|
71
|
+
typeof prefillEntityName === 'string' ? prefillEntityName.trim() : '';
|
|
72
|
+
if (trimmed) {
|
|
73
|
+
const resolved = resolvePrefillEntityName(trimmed, entities);
|
|
74
|
+
if (resolved) return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return promptForValidatedEntity(entities);
|
|
78
|
+
}
|
|
79
|
+
|
|
13
80
|
/**
|
|
14
81
|
* Handle entity selection step (OpenAPI multi-entity).
|
|
15
|
-
* Calls discover-entities;
|
|
82
|
+
* Calls discover-entities; prompts unless prefill or a single entity applies.
|
|
16
83
|
* @async
|
|
17
84
|
* @param {string} dataplaneUrl - Dataplane URL
|
|
18
85
|
* @param {Object} authConfig - Authentication configuration
|
|
19
86
|
* @param {Object} openapiSpec - OpenAPI specification
|
|
87
|
+
* @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
|
|
20
88
|
* @returns {Promise<string|null>} Selected entity name or null (skip)
|
|
21
89
|
*/
|
|
22
|
-
async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) {
|
|
90
|
+
async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
|
|
23
91
|
if (!openapiSpec || typeof openapiSpec !== 'object') return null;
|
|
24
92
|
try {
|
|
25
|
-
|
|
26
|
-
const entities = response?.data?.entities;
|
|
27
|
-
if (!Array.isArray(entities) || entities.length === 0) return null;
|
|
28
|
-
|
|
29
|
-
logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
|
|
30
|
-
const entityName = await promptForEntitySelection(entities);
|
|
31
|
-
const validation = validateEntityNameForOpenApi(entityName, entities);
|
|
32
|
-
if (!validation.valid) {
|
|
33
|
-
throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
|
|
34
|
-
}
|
|
35
|
-
logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
|
|
36
|
-
return entityName;
|
|
93
|
+
return await discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName);
|
|
37
94
|
} catch (error) {
|
|
38
95
|
logger.log(chalk.yellow(`Warning: Entity discovery failed, using default: ${error.message}`));
|
|
39
96
|
return null;
|
|
@@ -111,8 +111,11 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
|
|
|
111
111
|
systemConfig,
|
|
112
112
|
datasourceConfigs,
|
|
113
113
|
systemKey || appName,
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
{
|
|
115
|
+
dataplaneUrl,
|
|
116
|
+
authConfig,
|
|
117
|
+
enableRBAC: Boolean(preferences?.enableRBAC)
|
|
118
|
+
}
|
|
116
119
|
);
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -47,6 +47,12 @@ function buildSourceForSave(source) {
|
|
|
47
47
|
out.token = source.token ? '(set)' : undefined;
|
|
48
48
|
}
|
|
49
49
|
if (source.type === 'known-platform' && source.platform) out.platform = source.platform;
|
|
50
|
+
if (
|
|
51
|
+
(source.type === 'openapi-file' || source.type === 'openapi-url') &&
|
|
52
|
+
source.entityName
|
|
53
|
+
) {
|
|
54
|
+
out.entityName = source.entityName;
|
|
55
|
+
}
|
|
50
56
|
return out;
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -75,7 +81,13 @@ function buildWizardStateForSave(opts) {
|
|
|
75
81
|
function formatSourceLine(source) {
|
|
76
82
|
if (!source) return null;
|
|
77
83
|
const s = source;
|
|
78
|
-
|
|
84
|
+
let line =
|
|
85
|
+
s.type +
|
|
86
|
+
(s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
|
|
87
|
+
if (s.entityName && (s.type === 'openapi-file' || s.type === 'openapi-url')) {
|
|
88
|
+
line += ` [entity: ${s.entityName}]`;
|
|
89
|
+
}
|
|
90
|
+
return line;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
/**
|