@aifabrix/builder 2.21.1 → 2.22.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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * External System Generator Functions
3
+ *
4
+ * Functions for generating deployment JSON for external system applications.
5
+ *
6
+ * @fileoverview External system generator functions for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const Ajv = require('ajv');
14
+ const { detectAppType, getDeployJsonPath } = require('./utils/paths');
15
+ const { loadVariables, loadRbac } = require('./generator-helpers');
16
+
17
+ /**
18
+ * Generates external system <app-name>-deploy.json by loading the system JSON file
19
+ * For external systems, the system JSON file is already created and we just need to reference it
20
+ * @async
21
+ * @function generateExternalSystemDeployJson
22
+ * @param {string} appName - Name of the application
23
+ * @param {string} appPath - Path to application directory (integration or builder)
24
+ * @returns {Promise<string>} Path to generated <app-name>-deploy.json file
25
+ * @throws {Error} If generation fails
26
+ */
27
+ async function generateExternalSystemDeployJson(appName, appPath) {
28
+ if (!appName || typeof appName !== 'string') {
29
+ throw new Error('App name is required and must be a string');
30
+ }
31
+
32
+ const variablesPath = path.join(appPath, 'variables.yaml');
33
+ const { parsed: variables } = loadVariables(variablesPath);
34
+
35
+ if (!variables.externalIntegration) {
36
+ throw new Error('externalIntegration block not found in variables.yaml');
37
+ }
38
+
39
+ // For external systems, the system JSON file should be in the same folder
40
+ // Check if it already exists (should be <app-name>-deploy.json)
41
+ const deployJsonPath = getDeployJsonPath(appName, 'external', true);
42
+ const systemFileName = variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0
43
+ ? variables.externalIntegration.systems[0]
44
+ : `${appName}-deploy.json`;
45
+
46
+ // Resolve system file path (schemaBasePath is usually './' for same folder)
47
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
48
+ const systemFilePath = path.isAbsolute(schemaBasePath)
49
+ ? path.join(schemaBasePath, systemFileName)
50
+ : path.join(appPath, schemaBasePath, systemFileName);
51
+
52
+ // If system file doesn't exist, throw error (it should be created manually or via external-system-generator)
53
+ if (!fs.existsSync(systemFilePath)) {
54
+ throw new Error(`External system file not found: ${systemFilePath}. Please create it first.`);
55
+ }
56
+
57
+ // Read the system JSON file
58
+ const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
59
+ const systemJson = JSON.parse(systemContent);
60
+
61
+ // Load rbac.yaml from app directory (similar to regular apps)
62
+ const rbacPath = path.join(appPath, 'rbac.yaml');
63
+ const rbac = loadRbac(rbacPath);
64
+
65
+ // Merge rbac into systemJson if present
66
+ // Priority: roles/permissions in system JSON > rbac.yaml (if both exist, prefer JSON)
67
+ if (rbac) {
68
+ if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
69
+ systemJson.roles = rbac.roles;
70
+ }
71
+ if (rbac.permissions && (!systemJson.permissions || systemJson.permissions.length === 0)) {
72
+ systemJson.permissions = rbac.permissions;
73
+ }
74
+ }
75
+
76
+ // Write it as <app-name>-deploy.json (consistent naming)
77
+ const jsonContent = JSON.stringify(systemJson, null, 2);
78
+ await fs.promises.writeFile(deployJsonPath, jsonContent, { mode: 0o644, encoding: 'utf8' });
79
+
80
+ return deployJsonPath;
81
+ }
82
+
83
+ /**
84
+ * Generates application-schema.json structure for external systems
85
+ * Combines system and datasource JSONs into application-level deployment format
86
+ * @async
87
+ * @function generateExternalSystemApplicationSchema
88
+ * @param {string} appName - Application name
89
+ * @returns {Promise<Object>} Application schema object
90
+ * @throws {Error} If generation fails
91
+ */
92
+ async function generateExternalSystemApplicationSchema(appName) {
93
+ if (!appName || typeof appName !== 'string') {
94
+ throw new Error('App name is required and must be a string');
95
+ }
96
+
97
+ const { appPath } = await detectAppType(appName);
98
+ const variablesPath = path.join(appPath, 'variables.yaml');
99
+
100
+ // Load variables.yaml
101
+ const { parsed: variables } = loadVariables(variablesPath);
102
+
103
+ if (!variables.externalIntegration) {
104
+ throw new Error('externalIntegration block not found in variables.yaml');
105
+ }
106
+
107
+ // Load system file
108
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
109
+ const systemFiles = variables.externalIntegration.systems || [];
110
+
111
+ if (systemFiles.length === 0) {
112
+ throw new Error('No system files specified in externalIntegration.systems');
113
+ }
114
+
115
+ const systemFileName = systemFiles[0];
116
+ const systemFilePath = path.isAbsolute(schemaBasePath)
117
+ ? path.join(schemaBasePath, systemFileName)
118
+ : path.join(appPath, schemaBasePath, systemFileName);
119
+
120
+ if (!fs.existsSync(systemFilePath)) {
121
+ throw new Error(`System file not found: ${systemFilePath}`);
122
+ }
123
+
124
+ const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
125
+ const systemJson = JSON.parse(systemContent);
126
+
127
+ // Load rbac.yaml from app directory and merge if present
128
+ // Priority: roles/permissions in system JSON > rbac.yaml (if both exist, prefer JSON)
129
+ const rbacPath = path.join(appPath, 'rbac.yaml');
130
+ const rbac = loadRbac(rbacPath);
131
+ if (rbac) {
132
+ if (rbac.roles && (!systemJson.roles || systemJson.roles.length === 0)) {
133
+ systemJson.roles = rbac.roles;
134
+ }
135
+ if (rbac.permissions && (!systemJson.permissions || systemJson.permissions.length === 0)) {
136
+ systemJson.permissions = rbac.permissions;
137
+ }
138
+ }
139
+
140
+ // Load datasource files
141
+ const datasourceFiles = variables.externalIntegration.dataSources || [];
142
+ const datasourceJsons = [];
143
+
144
+ for (const datasourceFile of datasourceFiles) {
145
+ const datasourcePath = path.isAbsolute(schemaBasePath)
146
+ ? path.join(schemaBasePath, datasourceFile)
147
+ : path.join(appPath, schemaBasePath, datasourceFile);
148
+
149
+ if (!fs.existsSync(datasourcePath)) {
150
+ throw new Error(`Datasource file not found: ${datasourcePath}`);
151
+ }
152
+
153
+ const datasourceContent = await fs.promises.readFile(datasourcePath, 'utf8');
154
+ const datasourceJson = JSON.parse(datasourceContent);
155
+ datasourceJsons.push(datasourceJson);
156
+ }
157
+
158
+ // Build application-schema.json structure
159
+ const applicationSchema = {
160
+ version: variables.externalIntegration.version || '1.0.0',
161
+ application: systemJson,
162
+ dataSources: datasourceJsons
163
+ };
164
+
165
+ // Validate individual components against their schemas
166
+ const externalSystemSchema = require('./schema/external-system.schema.json');
167
+ const externalDatasourceSchema = require('./schema/external-datasource.schema.json');
168
+
169
+ // For draft-2020-12 schemas, remove $schema to avoid AJV issues (similar to schema-loader.js)
170
+ const datasourceSchemaToAdd = { ...externalDatasourceSchema };
171
+ if (datasourceSchemaToAdd.$schema && datasourceSchemaToAdd.$schema.includes('2020-12')) {
172
+ delete datasourceSchemaToAdd.$schema;
173
+ }
174
+
175
+ const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });
176
+
177
+ // Validate application (system) against external-system schema
178
+ const externalSystemSchemaId = externalSystemSchema.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-system.schema.json';
179
+ ajv.addSchema(externalSystemSchema, externalSystemSchemaId);
180
+ const validateSystem = ajv.compile(externalSystemSchema);
181
+ const systemValid = validateSystem(systemJson);
182
+
183
+ if (!systemValid) {
184
+ // Filter out additionalProperties errors for required properties that aren't defined in schema
185
+ // This handles schema inconsistencies where authentication is required but not defined in properties
186
+ const filteredErrors = validateSystem.errors.filter(err => {
187
+ if (err.keyword === 'additionalProperties' && err.params?.additionalProperty === 'authentication') {
188
+ // Check if authentication is in required array
189
+ const required = externalSystemSchema.required || [];
190
+ if (required.includes('authentication')) {
191
+ return false; // Ignore this error since authentication is required but not defined
192
+ }
193
+ }
194
+ return true;
195
+ });
196
+
197
+ if (filteredErrors.length > 0) {
198
+ const errors = filteredErrors.map(err => {
199
+ const path = err.instancePath || err.schemaPath;
200
+ return `${path} ${err.message}`;
201
+ }).join(', ');
202
+ throw new Error(`System JSON does not match external-system schema: ${errors}`);
203
+ }
204
+ }
205
+
206
+ // Validate each datasource against external-datasource schema
207
+ const externalDatasourceSchemaId = datasourceSchemaToAdd.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-datasource.schema.json';
208
+ ajv.addSchema(datasourceSchemaToAdd, externalDatasourceSchemaId);
209
+ const validateDatasource = ajv.compile(datasourceSchemaToAdd);
210
+
211
+ for (let i = 0; i < datasourceJsons.length; i++) {
212
+ const datasourceValid = validateDatasource(datasourceJsons[i]);
213
+ if (!datasourceValid) {
214
+ const errors = validateDatasource.errors.map(err => {
215
+ const path = err.instancePath || err.schemaPath;
216
+ return `${path} ${err.message}`;
217
+ }).join(', ');
218
+ throw new Error(`Datasource ${i + 1} (${datasourceJsons[i].key || 'unknown'}) does not match external-datasource schema: ${errors}`);
219
+ }
220
+ }
221
+
222
+ return applicationSchema;
223
+ }
224
+
225
+ module.exports = {
226
+ generateExternalSystemDeployJson,
227
+ generateExternalSystemApplicationSchema
228
+ };
229
+
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Generator Helper Functions
3
+ *
4
+ * Helper functions for loading and parsing configuration files used in deployment JSON generation.
5
+ *
6
+ * @fileoverview Generator helper functions for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const yaml = require('js-yaml');
13
+
14
+ /**
15
+ * Loads variables.yaml file
16
+ * @param {string} variablesPath - Path to variables.yaml
17
+ * @returns {Object} Parsed variables
18
+ * @throws {Error} If file not found or invalid YAML
19
+ */
20
+ function loadVariables(variablesPath) {
21
+ if (!fs.existsSync(variablesPath)) {
22
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
23
+ }
24
+
25
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
26
+ try {
27
+ return { content: variablesContent, parsed: yaml.load(variablesContent) };
28
+ } catch (error) {
29
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Loads env.template file
35
+ * @param {string} templatePath - Path to env.template
36
+ * @returns {string} Template content
37
+ * @throws {Error} If file not found
38
+ */
39
+ function loadEnvTemplate(templatePath) {
40
+ if (!fs.existsSync(templatePath)) {
41
+ throw new Error(`env.template not found: ${templatePath}`);
42
+ }
43
+ return fs.readFileSync(templatePath, 'utf8');
44
+ }
45
+
46
+ /**
47
+ * Loads rbac.yaml file if it exists
48
+ * @param {string} rbacPath - Path to rbac.yaml
49
+ * @returns {Object|null} Parsed RBAC configuration or null
50
+ * @throws {Error} If file exists but has invalid YAML
51
+ */
52
+ function loadRbac(rbacPath) {
53
+ if (!fs.existsSync(rbacPath)) {
54
+ return null;
55
+ }
56
+
57
+ const rbacContent = fs.readFileSync(rbacPath, 'utf8');
58
+ try {
59
+ return yaml.load(rbacContent);
60
+ } catch (error) {
61
+ throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Validates portalInput structure against schema requirements
67
+ * @param {Object} portalInput - Portal input configuration to validate
68
+ * @param {string} variableName - Variable name for error messages
69
+ * @throws {Error} If portalInput structure is invalid
70
+ */
71
+ function validatePortalInput(portalInput, variableName) {
72
+ if (!portalInput || typeof portalInput !== 'object') {
73
+ throw new Error(`Invalid portalInput for variable '${variableName}': must be an object`);
74
+ }
75
+
76
+ // Check required fields
77
+ if (!portalInput.field || typeof portalInput.field !== 'string') {
78
+ throw new Error(`Invalid portalInput for variable '${variableName}': field is required and must be a string`);
79
+ }
80
+
81
+ if (!portalInput.label || typeof portalInput.label !== 'string') {
82
+ throw new Error(`Invalid portalInput for variable '${variableName}': label is required and must be a string`);
83
+ }
84
+
85
+ // Validate field type
86
+ const validFieldTypes = ['password', 'text', 'textarea', 'select'];
87
+ if (!validFieldTypes.includes(portalInput.field)) {
88
+ throw new Error(`Invalid portalInput for variable '${variableName}': field must be one of: ${validFieldTypes.join(', ')}`);
89
+ }
90
+
91
+ // Validate select field requires options
92
+ if (portalInput.field === 'select') {
93
+ if (!portalInput.options || !Array.isArray(portalInput.options) || portalInput.options.length === 0) {
94
+ throw new Error(`Invalid portalInput for variable '${variableName}': select field requires a non-empty options array`);
95
+ }
96
+ }
97
+
98
+ // Validate optional fields
99
+ if (portalInput.placeholder !== undefined && typeof portalInput.placeholder !== 'string') {
100
+ throw new Error(`Invalid portalInput for variable '${variableName}': placeholder must be a string`);
101
+ }
102
+
103
+ if (portalInput.masked !== undefined && typeof portalInput.masked !== 'boolean') {
104
+ throw new Error(`Invalid portalInput for variable '${variableName}': masked must be a boolean`);
105
+ }
106
+
107
+ if (portalInput.validation !== undefined) {
108
+ if (typeof portalInput.validation !== 'object' || Array.isArray(portalInput.validation)) {
109
+ throw new Error(`Invalid portalInput for variable '${variableName}': validation must be an object`);
110
+ }
111
+ }
112
+
113
+ if (portalInput.options !== undefined && portalInput.field !== 'select') {
114
+ // Options should only be present for select fields
115
+ if (Array.isArray(portalInput.options) && portalInput.options.length > 0) {
116
+ throw new Error(`Invalid portalInput for variable '${variableName}': options can only be used with select field type`);
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Parses environment variables from env.template and merges portalInput from variables.yaml
123
+ * @param {string} envTemplate - Content of env.template file
124
+ * @param {Object|null} [variablesConfig=null] - Optional configuration from variables.yaml
125
+ * @returns {Array<Object>} Configuration array with merged portalInput
126
+ * @throws {Error} If portalInput structure is invalid
127
+ */
128
+ function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
129
+ const configuration = [];
130
+ const lines = envTemplate.split('\n');
131
+
132
+ // Create a map of portalInput configurations by variable name
133
+ const portalInputMap = new Map();
134
+ if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
135
+ for (const configItem of variablesConfig.configuration) {
136
+ if (configItem.name && configItem.portalInput) {
137
+ // Validate portalInput before adding to map
138
+ validatePortalInput(configItem.portalInput, configItem.name);
139
+ portalInputMap.set(configItem.name, configItem.portalInput);
140
+ }
141
+ }
142
+ }
143
+
144
+ for (const line of lines) {
145
+ const trimmed = line.trim();
146
+
147
+ // Skip empty lines and comments
148
+ if (!trimmed || trimmed.startsWith('#')) {
149
+ continue;
150
+ }
151
+
152
+ // Parse KEY=VALUE format
153
+ const equalIndex = trimmed.indexOf('=');
154
+ if (equalIndex === -1) {
155
+ continue;
156
+ }
157
+
158
+ const key = trimmed.substring(0, equalIndex).trim();
159
+ const value = trimmed.substring(equalIndex + 1).trim();
160
+
161
+ if (!key || !value) {
162
+ continue;
163
+ }
164
+
165
+ // Determine location and required status
166
+ let location = 'variable';
167
+ let required = false;
168
+
169
+ if (value.startsWith('kv://')) {
170
+ location = 'keyvault';
171
+ required = true;
172
+ }
173
+
174
+ // Check if it's a sensitive variable
175
+ const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
176
+ if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
177
+ required = true;
178
+ }
179
+
180
+ const configItem = {
181
+ name: key,
182
+ value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
183
+ location,
184
+ required
185
+ };
186
+
187
+ // Merge portalInput if it exists in variables.yaml
188
+ if (portalInputMap.has(key)) {
189
+ configItem.portalInput = portalInputMap.get(key);
190
+ }
191
+
192
+ configuration.push(configItem);
193
+ }
194
+
195
+ return configuration;
196
+ }
197
+
198
+ module.exports = {
199
+ loadVariables,
200
+ loadEnvTemplate,
201
+ loadRbac,
202
+ validatePortalInput,
203
+ parseEnvironmentVariables
204
+ };
205
+