@aifabrix/builder 2.32.3 → 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 (123) 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/types/wizard.types.js +176 -38
  40. package/lib/api/wizard.api.js +161 -23
  41. package/lib/app/deploy.js +116 -54
  42. package/lib/app/display.js +6 -5
  43. package/lib/app/dockerfile.js +2 -1
  44. package/lib/app/list.js +17 -10
  45. package/lib/app/readme.js +41 -112
  46. package/lib/app/register.js +44 -9
  47. package/lib/app/rotate-secret.js +48 -31
  48. package/lib/cli.js +219 -70
  49. package/lib/commands/app.js +4 -9
  50. package/lib/commands/auth-config.js +125 -0
  51. package/lib/commands/auth-status.js +7 -8
  52. package/lib/commands/datasource.js +3 -6
  53. package/lib/commands/login-credentials.js +4 -4
  54. package/lib/commands/login-device.js +26 -17
  55. package/lib/commands/login.js +12 -10
  56. package/lib/commands/wizard-config-normalizer.js +92 -0
  57. package/lib/commands/wizard-core.js +515 -0
  58. package/lib/commands/wizard-dataplane.js +122 -0
  59. package/lib/commands/wizard-headless.js +115 -0
  60. package/lib/commands/wizard.js +110 -332
  61. package/lib/core/config.js +46 -0
  62. package/lib/core/secrets.js +3 -22
  63. package/lib/core/templates-env.js +1 -1
  64. package/lib/datasource/deploy.js +29 -21
  65. package/lib/datasource/list.js +8 -6
  66. package/lib/deployment/deployer.js +25 -0
  67. package/lib/deployment/environment.js +10 -13
  68. package/lib/external-system/delete.js +151 -0
  69. package/lib/external-system/deploy.js +53 -378
  70. package/lib/external-system/download-helpers.js +45 -65
  71. package/lib/external-system/download.js +33 -13
  72. package/lib/external-system/generator.js +11 -7
  73. package/lib/external-system/test-auth.js +4 -3
  74. package/lib/generator/builders.js +3 -1
  75. package/lib/generator/external-controller-manifest.js +157 -0
  76. package/lib/generator/external-schema-utils.js +236 -0
  77. package/lib/generator/external.js +55 -3
  78. package/lib/generator/index.js +22 -10
  79. package/lib/generator/wizard-prompts.js +33 -10
  80. package/lib/generator/wizard.js +69 -86
  81. package/lib/infrastructure/compose.js +100 -0
  82. package/lib/infrastructure/helpers.js +139 -0
  83. package/lib/infrastructure/index.js +52 -311
  84. package/lib/infrastructure/services.js +168 -0
  85. package/lib/schema/application-schema.json +23 -4
  86. package/lib/schema/external-datasource.schema.json +2 -2
  87. package/lib/schema/wizard-config.schema.json +234 -0
  88. package/lib/utils/api.js +32 -50
  89. package/lib/utils/app-existence.js +42 -0
  90. package/lib/utils/app-register-config.js +7 -2
  91. package/lib/utils/auth-config-validator.js +92 -0
  92. package/lib/utils/command-header.js +43 -0
  93. package/lib/utils/compose-generator.js +113 -70
  94. package/lib/utils/controller-url.js +65 -17
  95. package/lib/utils/dataplane-health.js +115 -0
  96. package/lib/utils/dataplane-resolver.js +29 -0
  97. package/lib/utils/dev-config.js +6 -2
  98. package/lib/utils/env-copy.js +2 -1
  99. package/lib/utils/env-ports.js +2 -1
  100. package/lib/utils/env-template.js +1 -1
  101. package/lib/utils/error-formatter.js +49 -0
  102. package/lib/utils/external-readme.js +125 -0
  103. package/lib/utils/help-builder.js +190 -0
  104. package/lib/utils/infra-status.js +13 -3
  105. package/lib/utils/paths.js +17 -2
  106. package/lib/utils/port-resolver.js +111 -0
  107. package/lib/utils/secrets-helpers.js +3 -15
  108. package/lib/utils/secrets-utils.js +2 -2
  109. package/lib/utils/token-manager.js +9 -4
  110. package/lib/utils/variable-transformer.js +7 -2
  111. package/lib/validation/external-manifest-validator.js +202 -0
  112. package/lib/validation/validate-display.js +406 -0
  113. package/lib/validation/validate.js +159 -123
  114. package/lib/validation/validator.js +36 -3
  115. package/lib/validation/wizard-config-validator.js +267 -0
  116. package/package.json +4 -2
  117. package/templates/applications/README.md.hbs +18 -16
  118. package/templates/applications/miso-controller/env.template +1 -1
  119. package/templates/applications/miso-controller/rbac.yaml +7 -7
  120. package/templates/external-system/README.md.hbs +99 -0
  121. package/templates/infra/compose.yaml.hbs +35 -0
  122. package/templates/python/docker-compose.hbs +26 -0
  123. package/templates/typescript/docker-compose.hbs +26 -0
@@ -16,7 +16,6 @@ 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');
@@ -125,17 +124,19 @@ function handlePartialDownload(systemKey, systemData, datasourceErrors) {
125
124
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
126
125
  * @throws {Error} If authentication fails
127
126
  */
128
- async function setupAuthenticationAndDataplane(systemKey, options, config) {
129
- const environment = options.environment || 'dev';
130
- const controllerUrl = await resolveControllerUrl(options, config);
127
+ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
128
+ const { resolveEnvironment } = require('../core/config');
129
+ const environment = await resolveEnvironment();
130
+ const controllerUrl = await resolveControllerUrl();
131
131
  const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
132
132
 
133
133
  if (!authConfig.token && !authConfig.clientId) {
134
134
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
135
135
  }
136
136
 
137
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
138
- 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);
139
140
  logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
140
141
 
141
142
  return { authConfig, dataplaneUrl };
@@ -159,8 +160,19 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
159
160
  }
160
161
 
161
162
  const downloadData = response.data.data || response.data;
162
- const application = downloadData.application;
163
- 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
+ }
164
176
 
165
177
  if (!application) {
166
178
  throw new Error('Application configuration not found in download response');
@@ -189,7 +201,7 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
189
201
  * @returns {Promise<string>} System file path
190
202
  */
191
203
  async function generateSystemFile(tempDir, systemKey, application) {
192
- const systemFileName = `${systemKey}-deploy.json`;
204
+ const systemFileName = `${systemKey}-system.json`;
193
205
  const systemFilePath = path.join(tempDir, systemFileName);
194
206
  await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
195
207
  return systemFilePath;
@@ -209,8 +221,16 @@ async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
209
221
  const datasourceFiles = [];
210
222
  for (const datasource of dataSources) {
211
223
  try {
212
- const entityType = datasource.entityType || datasource.entityKey || datasource.key.split('-').pop();
213
- 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`;
214
234
  const datasourceFilePath = path.join(tempDir, datasourceFileName);
215
235
  await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
216
236
  datasourceFiles.push(datasourceFilePath);
@@ -284,7 +304,7 @@ async function moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths
284
304
  logger.log(chalk.blue(`📁 Creating directory: ${finalPath}`));
285
305
  await fs.mkdir(finalPath, { recursive: true });
286
306
 
287
- const systemFileName = `${systemKey}-deploy.json`;
307
+ const systemFileName = `${systemKey}-system.json`;
288
308
  const filesToMove = [
289
309
  { from: filePaths.systemFilePath, to: path.join(finalPath, systemFileName) },
290
310
  { from: filePaths.variablesPath, to: path.join(finalPath, 'variables.yaml') },
@@ -328,7 +348,7 @@ function handleDryRun(systemKey, dataplaneUrl) {
328
348
  logger.log(chalk.yellow('\nWould create:'));
329
349
  logger.log(chalk.gray(` integration/${systemKey}/`));
330
350
  logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
331
- logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
351
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-system.json`));
332
352
  logger.log(chalk.gray(` integration/${systemKey}/env.template`));
333
353
  logger.log(chalk.gray(` integration/${systemKey}/README.md`));
334
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'
@@ -21,9 +21,10 @@ const { resolveControllerUrl } = require('../utils/controller-url');
21
21
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
22
22
  * @throws {Error} If authentication fails
23
23
  */
24
- async function setupIntegrationTestAuth(appName, options, config) {
25
- const environment = options.environment || 'dev';
26
- const controllerUrl = await resolveControllerUrl(options, config);
24
+ async function setupIntegrationTestAuth(appName, _options, _config) {
25
+ const { resolveEnvironment } = require('../core/config');
26
+ const environment = await resolveEnvironment();
27
+ const controllerUrl = await resolveControllerUrl();
27
28
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
28
29
 
29
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