@aifabrix/builder 2.32.2 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
@@ -16,12 +16,12 @@ const yaml = require('js-yaml');
16
16
  const chalk = require('chalk');
17
17
  const { getExternalSystemConfig } = require('../api/external-systems.api');
18
18
  const { getDeploymentAuth } = require('../utils/token-manager');
19
- const { getDataplaneUrl } = require('../datasource/deploy');
20
19
  const { getConfig } = require('../core/config');
21
20
  const { detectAppType } = require('../utils/paths');
22
21
  const logger = require('../utils/logger');
23
22
  const { generateEnvTemplate } = require('../utils/external-system-env-helpers');
24
23
  const { generateVariablesYaml, generateReadme } = require('./download-helpers');
24
+ const { resolveControllerUrl } = require('../utils/controller-url');
25
25
 
26
26
  /**
27
27
  * Validates system type from downloaded application
@@ -124,17 +124,19 @@ function handlePartialDownload(systemKey, systemData, datasourceErrors) {
124
124
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
125
125
  * @throws {Error} If authentication fails
126
126
  */
127
- async function setupAuthenticationAndDataplane(systemKey, options, config) {
128
- const environment = options.environment || 'dev';
129
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
127
+ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
128
+ const { resolveEnvironment } = require('../core/config');
129
+ const environment = await resolveEnvironment();
130
+ const controllerUrl = await resolveControllerUrl();
130
131
  const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
131
132
 
132
133
  if (!authConfig.token && !authConfig.clientId) {
133
134
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
134
135
  }
135
136
 
136
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
137
- const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
137
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
138
+ logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
139
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
138
140
  logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
139
141
 
140
142
  return { authConfig, dataplaneUrl };
@@ -158,8 +160,19 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
158
160
  }
159
161
 
160
162
  const downloadData = response.data.data || response.data;
161
- const application = downloadData.application;
162
- const dataSources = downloadData.dataSources || [];
163
+ let application = downloadData.application;
164
+ let dataSources = downloadData.dataSources || [];
165
+
166
+ // Handle case where datasources are inline in application.configuration.dataSources
167
+ if (!application && downloadData.configuration) {
168
+ application = downloadData;
169
+ }
170
+ if (application && application.configuration && Array.isArray(application.configuration.dataSources)) {
171
+ dataSources = [...dataSources, ...application.configuration.dataSources];
172
+ // Remove inline datasources from application to avoid duplication
173
+ const { dataSources: _inlineDataSources, ...configWithoutDataSources } = application.configuration;
174
+ application = { ...application, configuration: configWithoutDataSources };
175
+ }
163
176
 
164
177
  if (!application) {
165
178
  throw new Error('Application configuration not found in download response');
@@ -188,7 +201,7 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
188
201
  * @returns {Promise<string>} System file path
189
202
  */
190
203
  async function generateSystemFile(tempDir, systemKey, application) {
191
- const systemFileName = `${systemKey}-deploy.json`;
204
+ const systemFileName = `${systemKey}-system.json`;
192
205
  const systemFilePath = path.join(tempDir, systemFileName);
193
206
  await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
194
207
  return systemFilePath;
@@ -208,8 +221,16 @@ async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
208
221
  const datasourceFiles = [];
209
222
  for (const datasource of dataSources) {
210
223
  try {
211
- const entityType = datasource.entityType || datasource.entityKey || datasource.key.split('-').pop();
212
- const datasourceFileName = `${systemKey}-deploy-${entityType}.json`;
224
+ const datasourceKey = datasource.key || '';
225
+ // Extract datasource key (remove system key prefix if present)
226
+ let datasourceKeyOnly;
227
+ if (datasourceKey.startsWith(`${systemKey}-`)) {
228
+ datasourceKeyOnly = datasourceKey.substring(systemKey.length + 1);
229
+ } else {
230
+ const entityType = datasource.entityType || datasource.entityKey || datasourceKey.split('-').pop();
231
+ datasourceKeyOnly = entityType;
232
+ }
233
+ const datasourceFileName = `${systemKey}-datasource-${datasourceKeyOnly}.json`;
213
234
  const datasourceFilePath = path.join(tempDir, datasourceFileName);
214
235
  await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
215
236
  datasourceFiles.push(datasourceFilePath);
@@ -283,7 +304,7 @@ async function moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths
283
304
  logger.log(chalk.blue(`📁 Creating directory: ${finalPath}`));
284
305
  await fs.mkdir(finalPath, { recursive: true });
285
306
 
286
- const systemFileName = `${systemKey}-deploy.json`;
307
+ const systemFileName = `${systemKey}-system.json`;
287
308
  const filesToMove = [
288
309
  { from: filePaths.systemFilePath, to: path.join(finalPath, systemFileName) },
289
310
  { from: filePaths.variablesPath, to: path.join(finalPath, 'variables.yaml') },
@@ -327,7 +348,7 @@ function handleDryRun(systemKey, dataplaneUrl) {
327
348
  logger.log(chalk.yellow('\nWould create:'));
328
349
  logger.log(chalk.gray(` integration/${systemKey}/`));
329
350
  logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
330
- logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
351
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-system.json`));
331
352
  logger.log(chalk.gray(` integration/${systemKey}/env.template`));
332
353
  logger.log(chalk.gray(` integration/${systemKey}/README.md`));
333
354
  }
@@ -48,8 +48,8 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
48
48
  const rendered = template(context);
49
49
 
50
50
  // Generate in same folder as variables.yaml (new structure)
51
- // Use naming: <app-name>-deploy.json
52
- const outputPath = path.join(appPath, `${systemKey}-deploy.json`);
51
+ // Use naming: <app-name>-system.json
52
+ const outputPath = path.join(appPath, `${systemKey}-system.json`);
53
53
  await fs.writeFile(outputPath, rendered, 'utf8');
54
54
 
55
55
  return outputPath;
@@ -89,8 +89,12 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
89
89
  const rendered = template(context);
90
90
 
91
91
  // Generate in same folder as variables.yaml (new structure)
92
- // Use naming: <app-name>-deploy-<datasource-key>.json
93
- const outputPath = path.join(appPath, `${datasourceKey}-deploy.json`);
92
+ // Use naming: <app-name>-datasource-<datasource-key>.json
93
+ // Extract datasource key (remove system key prefix if present)
94
+ const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${config.systemKey}-`)
95
+ ? datasourceKey.substring(config.systemKey.length + 1)
96
+ : datasourceKey;
97
+ const outputPath = path.join(appPath, `${config.systemKey}-datasource-${datasourceKeyOnly}.json`);
94
98
  await fs.writeFile(outputPath, rendered, 'utf8');
95
99
 
96
100
  return outputPath;
@@ -139,8 +143,8 @@ async function generateExternalSystemFiles(appPath, appName, config) {
139
143
  attributes: config.attributes || {}
140
144
  };
141
145
 
142
- // Generate with full naming: <app-name>-deploy-<entity-key>.json
143
- const datasourcePath = await generateExternalDataSourceTemplate(appPath, `${systemKey}-deploy-${datasourceKey}`, datasourceConfig);
146
+ // Generate with new naming: <app-name>-datasource-<entity-key>.json
147
+ const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig);
144
148
  datasourcePaths.push(datasourcePath);
145
149
  logger.log(chalk.green(`✓ Generated datasource: ${path.basename(datasourcePath)}`));
146
150
  }
@@ -176,7 +180,7 @@ async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, da
176
180
  // Files are in same folder, so schemaBasePath is './'
177
181
  variables.externalIntegration = {
178
182
  schemaBasePath: './',
179
- systems: [`${systemKey}-deploy.json`],
183
+ systems: [`${systemKey}-system.json`],
180
184
  dataSources: datasourcePaths.map(p => path.basename(p)),
181
185
  autopublish: true,
182
186
  version: '1.0.0'
@@ -10,6 +10,7 @@
10
10
 
11
11
  const { getDeploymentAuth } = require('../utils/token-manager');
12
12
  const { getDataplaneUrl } = require('../datasource/deploy');
13
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
14
 
14
15
  /**
15
16
  * Setup authentication and get dataplane URL for integration tests
@@ -20,9 +21,10 @@ const { getDataplaneUrl } = require('../datasource/deploy');
20
21
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
21
22
  * @throws {Error} If authentication fails
22
23
  */
23
- async function setupIntegrationTestAuth(appName, options, config) {
24
- const environment = options.environment || 'dev';
25
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
24
+ async function setupIntegrationTestAuth(appName, _options, _config) {
25
+ const { resolveEnvironment } = require('../core/config');
26
+ const environment = await resolveEnvironment();
27
+ const controllerUrl = await resolveControllerUrl();
26
28
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
27
29
 
28
30
  if (!authConfig.token && !authConfig.clientId) {
@@ -8,6 +8,8 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const { getContainerPort } = require('../utils/port-resolver');
12
+
11
13
  /**
12
14
  * Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
13
15
  * @function sanitizeAuthType
@@ -169,7 +171,7 @@ function buildBaseDeployment(appName, variables, filteredConfiguration) {
169
171
  return {
170
172
  ...appMetadata,
171
173
  ...imageConfig,
172
- port: variables.port || 3000,
174
+ port: getContainerPort(variables, 3000),
173
175
  ...requirementsConfig,
174
176
  configuration: filteredConfiguration
175
177
  };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * External System Controller Manifest Generator
3
+ *
4
+ * Generates controller-compatible deployment manifest for external systems.
5
+ * Creates manifest with inline system + dataSources in controller format.
6
+ *
7
+ * @fileoverview Controller manifest generation for external systems
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const path = require('path');
13
+ const { detectAppType } = require('../utils/paths');
14
+ const { generateDeploymentKeyFromJson } = require('../core/key-generator');
15
+ const { loadSystemFile, loadDatasourceFiles } = require('./external');
16
+ const { loadVariables, loadRbac } = require('./helpers');
17
+
18
+ /**
19
+ * Merges RBAC into system JSON
20
+ * @function mergeRbacIntoSystemJson
21
+ * @param {Object} systemJson - System JSON object
22
+ * @param {Object|null} rbac - RBAC configuration
23
+ */
24
+ function mergeRbacIntoSystemJson(systemJson, rbac) {
25
+ if (!rbac) {
26
+ return;
27
+ }
28
+
29
+ // Priority: roles/permissions in system JSON > rbac.yaml (if both exist, prefer JSON)
30
+ if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
31
+ systemJson.roles = rbac.roles;
32
+ }
33
+ if (rbac.permissions && (!systemJson.permissions || systemJson.permissions.length === 0)) {
34
+ systemJson.permissions = rbac.permissions;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Resolves application path from options or detection
40
+ * @async
41
+ * @function resolveAppPath
42
+ * @param {string} appName - Application name
43
+ * @param {Object} options - Options with optional appPath
44
+ * @returns {Promise<string>} Application path
45
+ */
46
+ async function resolveAppPath(appName, options) {
47
+ if (options.appPath) {
48
+ return options.appPath;
49
+ }
50
+ const detected = await detectAppType(appName, { type: 'external' });
51
+ return detected.appPath;
52
+ }
53
+
54
+ /**
55
+ * Extracts app metadata from variables
56
+ * @function extractAppMetadata
57
+ * @param {Object} variables - Parsed variables.yaml
58
+ * @param {string} appName - Application name
59
+ * @returns {Object} App metadata { appKey, displayName, description }
60
+ */
61
+ function extractAppMetadata(variables, appName) {
62
+ return {
63
+ appKey: variables.app?.key || appName,
64
+ displayName: variables.app?.displayName || appName,
65
+ description: variables.app?.description || `External system integration for ${appName}`
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Loads and merges system with RBAC
71
+ * @async
72
+ * @function loadSystemWithRbac
73
+ * @param {string} appPath - Application path
74
+ * @param {string} schemaBasePath - Schema base path
75
+ * @param {string} systemFile - System file path
76
+ * @returns {Promise<Object>} System JSON with RBAC merged
77
+ */
78
+ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
79
+ const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFile);
80
+ const rbacPath = path.join(appPath, 'rbac.yaml');
81
+ const rbac = loadRbac(rbacPath);
82
+ mergeRbacIntoSystemJson(systemJson, rbac);
83
+ return systemJson;
84
+ }
85
+
86
+ /**
87
+ * Generates controller-compatible deployment manifest for external systems
88
+ * Creates manifest with inline system + dataSources in controller format
89
+ *
90
+ * @async
91
+ * @function generateControllerManifest
92
+ * @param {string} appName - Application name
93
+ * @param {Object} [options] - Optional parameters
94
+ * @param {string} [options.appPath] - Application path (if provided, skips detection)
95
+ * @returns {Promise<Object>} Controller manifest object
96
+ * @throws {Error} If generation fails
97
+ *
98
+ * @example
99
+ * const manifest = await generateControllerManifest('my-hubspot');
100
+ * // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...], deploymentKey }
101
+ */
102
+ async function generateControllerManifest(appName, options = {}) {
103
+ if (!appName || typeof appName !== 'string') {
104
+ throw new Error('App name is required and must be a string');
105
+ }
106
+
107
+ const appPath = await resolveAppPath(appName, options);
108
+ const variablesPath = path.join(appPath, 'variables.yaml');
109
+ const { parsed: variables } = loadVariables(variablesPath);
110
+
111
+ if (!variables.externalIntegration) {
112
+ throw new Error('externalIntegration block not found in variables.yaml');
113
+ }
114
+
115
+ const metadata = extractAppMetadata(variables, appName);
116
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
117
+ const systemFiles = variables.externalIntegration.systems || [];
118
+
119
+ if (systemFiles.length === 0) {
120
+ throw new Error('No system files specified in externalIntegration.systems');
121
+ }
122
+
123
+ const systemJson = await loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]);
124
+ const datasourceFiles = variables.externalIntegration.dataSources || [];
125
+ const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
126
+
127
+ // Build externalIntegration block (required by application schema for type: "external")
128
+ const externalIntegration = {
129
+ schemaBasePath: schemaBasePath,
130
+ systems: systemFiles,
131
+ dataSources: datasourceFiles,
132
+ autopublish: variables.externalIntegration.autopublish !== false, // default true
133
+ version: variables.externalIntegration.version || '1.0.0'
134
+ };
135
+
136
+ const manifest = {
137
+ key: metadata.appKey,
138
+ displayName: metadata.displayName,
139
+ description: metadata.description,
140
+ type: 'external',
141
+ externalIntegration: externalIntegration,
142
+ // Inline system and dataSources for atomic deployment (optional but recommended)
143
+ system: systemJson,
144
+ dataSources: datasourceJsons,
145
+ // Explicitly set to false to satisfy conditional schema requirements
146
+ requiresDatabase: false,
147
+ requiresRedis: false,
148
+ requiresStorage: false
149
+ };
150
+
151
+ manifest.deploymentKey = generateDeploymentKeyFromJson(manifest);
152
+ return manifest;
153
+ }
154
+
155
+ module.exports = {
156
+ generateControllerManifest
157
+ };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * External Schema Split Utilities
3
+ *
4
+ * Helpers for splitting application-schema.json into component files.
5
+ *
6
+ * @fileoverview External schema split utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const { generateEnvTemplate } = require('../utils/external-system-env-helpers');
16
+ const { extractRbacYaml } = require('./split');
17
+ const { generateExternalReadmeContent } = require('../utils/external-readme');
18
+
19
+ /**
20
+ * Parses application-schema.json content
21
+ * @function parseApplicationSchema
22
+ * @param {string} schemaPath - Schema file path
23
+ * @param {string} content - Raw JSON content
24
+ * @returns {Object} Parsed schema details
25
+ * @throws {Error} If JSON or schema structure is invalid
26
+ */
27
+ function parseApplicationSchema(schemaPath, content) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(content);
31
+ } catch (error) {
32
+ throw new Error(`Invalid JSON syntax in ${schemaPath}: ${error.message}`);
33
+ }
34
+
35
+ const application = parsed.application;
36
+ const dataSources = Array.isArray(parsed.dataSources) ? parsed.dataSources : [];
37
+ if (!application || typeof application !== 'object') {
38
+ throw new Error('application-schema.json must include an "application" object');
39
+ }
40
+
41
+ return { application, dataSources, version: parsed.version };
42
+ }
43
+
44
+ /**
45
+ * Extracts system key from application schema
46
+ * @function getSystemKey
47
+ * @param {Object} application - Application schema object
48
+ * @returns {string} System key
49
+ * @throws {Error} If system key is missing
50
+ */
51
+ function getSystemKey(application) {
52
+ const systemKey = application.key;
53
+ if (!systemKey || typeof systemKey !== 'string') {
54
+ throw new Error('application.key is required to split external system files');
55
+ }
56
+ return systemKey;
57
+ }
58
+
59
+ /**
60
+ * Writes JSON file with formatting
61
+ * @async
62
+ * @function writeJsonFile
63
+ * @param {string} filePath - File path
64
+ * @param {Object} data - JSON data
65
+ * @returns {Promise<void>} Resolves when file is written
66
+ */
67
+ async function writeJsonFile(filePath, data) {
68
+ await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
69
+ }
70
+
71
+ /**
72
+ * Resolves datasource entity type
73
+ * @function getDatasourceEntityType
74
+ * @param {Object} datasource - Datasource schema
75
+ * @param {number} index - Datasource index
76
+ * @returns {string} Entity type
77
+ */
78
+ function getDatasourceEntityType(datasource, index) {
79
+ return (
80
+ datasource.entityType ||
81
+ datasource.entityKey ||
82
+ datasource.key?.split('-').pop() ||
83
+ `entity${index + 1}`
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Builds datasource file name
89
+ * @function getDatasourceFileName
90
+ * @param {string} systemKey - System key
91
+ * @param {Object} datasource - Datasource schema
92
+ * @param {number} index - Datasource index
93
+ * @returns {string} Datasource file name
94
+ */
95
+ function getDatasourceFileName(systemKey, datasource, index) {
96
+ const datasourceKey = datasource.key || '';
97
+ // Extract datasource key (remove system key prefix if present)
98
+ let datasourceKeyOnly;
99
+ if (datasourceKey.startsWith(`${systemKey}-`)) {
100
+ datasourceKeyOnly = datasourceKey.substring(systemKey.length + 1);
101
+ } else if (datasourceKey.startsWith(`${systemKey}-deploy-`)) {
102
+ // Support old naming format during migration
103
+ datasourceKeyOnly = datasourceKey.substring(`${systemKey}-deploy-`.length);
104
+ } else {
105
+ datasourceKeyOnly = getDatasourceEntityType(datasource, index);
106
+ }
107
+ return `${systemKey}-datasource-${datasourceKeyOnly}.json`;
108
+ }
109
+
110
+ /**
111
+ * Writes datasource files and returns file names
112
+ * @async
113
+ * @function writeDatasourceFiles
114
+ * @param {string} outputDir - Output directory
115
+ * @param {string} systemKey - System key
116
+ * @param {Object[]} dataSources - Datasource schemas
117
+ * @returns {Promise<string[]>} Datasource file names
118
+ */
119
+ async function writeDatasourceFiles(outputDir, systemKey, dataSources) {
120
+ const datasourceFileNames = [];
121
+ for (let i = 0; i < dataSources.length; i += 1) {
122
+ const datasource = dataSources[i];
123
+ const datasourceFileName = getDatasourceFileName(systemKey, datasource, i);
124
+ const datasourceFilePath = path.join(outputDir, datasourceFileName);
125
+ await writeJsonFile(datasourceFilePath, datasource);
126
+ datasourceFileNames.push(datasourceFileName);
127
+ }
128
+ return datasourceFileNames;
129
+ }
130
+
131
+ /**
132
+ * Builds variables.yaml content for external integrations
133
+ * @function buildExternalVariables
134
+ * @param {string} systemKey - System key
135
+ * @param {Object} application - Application schema
136
+ * @param {string} systemFileName - System file name
137
+ * @param {string[]} datasourceFileNames - Datasource file names
138
+ * @param {string} version - Schema version
139
+ * @returns {Object} Variables content
140
+ */
141
+ function buildExternalVariables(systemKey, application, systemFileName, datasourceFileNames, version) {
142
+ return {
143
+ app: {
144
+ key: systemKey,
145
+ displayName: application.displayName || systemKey,
146
+ description: application.description || `External system integration for ${systemKey}`,
147
+ type: 'external'
148
+ },
149
+ deployment: {
150
+ controllerUrl: '',
151
+ environment: 'dev'
152
+ },
153
+ externalIntegration: {
154
+ schemaBasePath: './',
155
+ systems: [systemFileName],
156
+ dataSources: datasourceFileNames,
157
+ autopublish: true,
158
+ version
159
+ }
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Writes YAML file
165
+ * @async
166
+ * @function writeYamlFile
167
+ * @param {string} filePath - File path
168
+ * @param {Object} data - YAML data
169
+ * @param {Object} options - YAML options
170
+ * @returns {Promise<void>} Resolves when file is written
171
+ */
172
+ async function writeYamlFile(filePath, data, options) {
173
+ const yamlContent = yaml.dump(data, options);
174
+ await fs.promises.writeFile(filePath, yamlContent, 'utf8');
175
+ }
176
+
177
+ /**
178
+ * Writes split files for external application schemas
179
+ * @async
180
+ * @function writeSplitExternalSchemaFiles
181
+ * @param {Object} params - Split parameters
182
+ * @param {string} params.outputDir - Output directory
183
+ * @param {string} params.systemKey - System key
184
+ * @param {Object} params.application - Application schema
185
+ * @param {Object[]} params.dataSources - Datasource schemas
186
+ * @param {string} params.version - Schema version
187
+ * @returns {Promise<Object>} Paths to generated files
188
+ */
189
+ async function writeSplitExternalSchemaFiles({ outputDir, systemKey, application, dataSources, version }) {
190
+ const systemFileName = `${systemKey}-system.json`;
191
+ const systemFilePath = path.join(outputDir, systemFileName);
192
+ await writeJsonFile(systemFilePath, application);
193
+
194
+ const datasourceFileNames = await writeDatasourceFiles(outputDir, systemKey, dataSources);
195
+ const variables = buildExternalVariables(systemKey, application, systemFileName, datasourceFileNames, version);
196
+
197
+ const variablesPath = path.join(outputDir, 'variables.yaml');
198
+ await writeYamlFile(variablesPath, variables, { indent: 2, lineWidth: 120, noRefs: true });
199
+
200
+ const envTemplatePath = path.join(outputDir, 'env.template');
201
+ const envTemplate = generateEnvTemplate(application);
202
+ await fs.promises.writeFile(envTemplatePath, envTemplate, 'utf8');
203
+
204
+ const rbac = extractRbacYaml(application);
205
+ let rbacPath = null;
206
+ if (rbac) {
207
+ rbacPath = path.join(outputDir, 'rbac.yml');
208
+ await writeYamlFile(rbacPath, rbac, { indent: 2, lineWidth: -1 });
209
+ }
210
+
211
+ const readmeContent = generateExternalReadmeContent({
212
+ appName: systemKey,
213
+ systemKey,
214
+ systemType: application.type,
215
+ displayName: application.displayName,
216
+ description: application.description,
217
+ datasources: dataSources
218
+ });
219
+ const readmePath = path.join(outputDir, 'README.md');
220
+ await fs.promises.writeFile(readmePath, readmeContent, 'utf8');
221
+
222
+ return {
223
+ systemFile: systemFilePath,
224
+ datasourceFiles: datasourceFileNames.map(name => path.join(outputDir, name)),
225
+ variables: variablesPath,
226
+ envTemplate: envTemplatePath,
227
+ rbac: rbacPath,
228
+ readme: readmePath
229
+ };
230
+ }
231
+
232
+ module.exports = {
233
+ parseApplicationSchema,
234
+ getSystemKey,
235
+ writeSplitExternalSchemaFiles
236
+ };
@@ -13,6 +13,11 @@ const path = require('path');
13
13
  const Ajv = require('ajv');
14
14
  const { detectAppType, getDeployJsonPath } = require('../utils/paths');
15
15
  const { loadVariables, loadRbac } = require('./helpers');
16
+ const {
17
+ parseApplicationSchema,
18
+ getSystemKey,
19
+ writeSplitExternalSchemaFiles
20
+ } = require('./external-schema-utils');
16
21
 
17
22
  /**
18
23
  * Generates external system <app-name>-deploy.json by loading the system JSON file
@@ -36,15 +41,26 @@ const { loadVariables, loadRbac } = require('./helpers');
36
41
  function resolveSystemFilePath(variables, appPath, appName) {
37
42
  const systemFileName = variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0
38
43
  ? variables.externalIntegration.systems[0]
39
- : `${appName}-deploy.json`;
44
+ : `${appName}-system.json`;
40
45
 
41
46
  const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
42
47
  const systemFilePath = path.isAbsolute(schemaBasePath)
43
48
  ? path.join(schemaBasePath, systemFileName)
44
49
  : path.join(appPath, schemaBasePath, systemFileName);
45
50
 
51
+ // Support both old and new naming for backward compatibility
46
52
  if (!fs.existsSync(systemFilePath)) {
47
- throw new Error(`External system file not found: ${systemFilePath}. Please create it first.`);
53
+ // Try old naming format
54
+ const oldSystemFileName = systemFileName.replace(/-system\.json$/, '-deploy.json');
55
+ const oldSystemFilePath = path.isAbsolute(schemaBasePath)
56
+ ? path.join(schemaBasePath, oldSystemFileName)
57
+ : path.join(appPath, schemaBasePath, oldSystemFileName);
58
+
59
+ if (fs.existsSync(oldSystemFilePath)) {
60
+ return oldSystemFilePath;
61
+ }
62
+
63
+ throw new Error(`External system file not found: ${systemFilePath} (also checked: ${oldSystemFilePath}). Please create it first.`);
48
64
  }
49
65
 
50
66
  return systemFilePath;
@@ -358,8 +374,44 @@ async function generateExternalSystemApplicationSchema(appName) {
358
374
  return applicationSchema;
359
375
  }
360
376
 
377
+ /**
378
+ * Splits application-schema.json into component files for external systems
379
+ * @async
380
+ * @function splitExternalApplicationSchema
381
+ * @param {string} schemaPath - Path to application-schema.json
382
+ * @param {string} outputDir - Output directory for split files
383
+ * @returns {Promise<Object>} Paths to generated files
384
+ * @throws {Error} If schema is invalid or files cannot be written
385
+ */
386
+ async function splitExternalApplicationSchema(schemaPath, outputDir) {
387
+ if (!schemaPath || typeof schemaPath !== 'string') {
388
+ throw new Error('Schema path is required and must be a string');
389
+ }
390
+ if (!outputDir || typeof outputDir !== 'string') {
391
+ throw new Error('Output directory is required and must be a string');
392
+ }
393
+
394
+ const content = await fs.promises.readFile(schemaPath, 'utf8');
395
+ const { application, dataSources, version } = parseApplicationSchema(schemaPath, content);
396
+ const systemKey = getSystemKey(application);
397
+
398
+ await fs.promises.mkdir(outputDir, { recursive: true });
399
+
400
+ const schemaVersion = version || application.version || '1.0.0';
401
+ return writeSplitExternalSchemaFiles({
402
+ outputDir,
403
+ systemKey,
404
+ application,
405
+ dataSources,
406
+ version: schemaVersion
407
+ });
408
+ }
409
+
361
410
  module.exports = {
362
411
  generateExternalSystemDeployJson,
363
- generateExternalSystemApplicationSchema
412
+ generateExternalSystemApplicationSchema,
413
+ splitExternalApplicationSchema,
414
+ loadSystemFile,
415
+ loadDatasourceFiles
364
416
  };
365
417