@aifabrix/builder 2.6.2 → 2.7.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.
- package/bin/aifabrix.js +4 -0
- package/lib/cli.js +51 -2
- package/lib/commands/datasource.js +94 -0
- package/lib/config.js +25 -36
- package/lib/datasource-deploy.js +182 -0
- package/lib/datasource-diff.js +73 -0
- package/lib/datasource-list.js +138 -0
- package/lib/datasource-validate.js +63 -0
- package/lib/diff.js +266 -0
- package/lib/schema/application-schema.json +829 -687
- package/lib/schema/external-datasource.schema.json +464 -0
- package/lib/schema/external-system.schema.json +262 -0
- package/lib/secrets.js +20 -1
- package/lib/utils/api.js +19 -3
- package/lib/utils/env-copy.js +24 -0
- package/lib/utils/env-endpoints.js +50 -11
- package/lib/utils/schema-loader.js +220 -0
- package/lib/utils/schema-resolver.js +174 -0
- package/lib/utils/secrets-helpers.js +65 -17
- package/lib/utils/token-manager.js +57 -21
- package/lib/validate.js +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates external datasource JSON files against schema.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Datasource validation for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { loadExternalDataSourceSchema } = require('./utils/schema-loader');
|
|
13
|
+
const { formatValidationErrors } = require('./utils/error-formatter');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates a datasource file against external-datasource schema
|
|
17
|
+
*
|
|
18
|
+
* @async
|
|
19
|
+
* @function validateDatasourceFile
|
|
20
|
+
* @param {string} filePath - Path to the datasource JSON file
|
|
21
|
+
* @returns {Promise<Object>} Validation result with errors and warnings
|
|
22
|
+
* @throws {Error} If file cannot be read or parsed
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const result = await validateDatasourceFile('./hubspot-deal.json');
|
|
26
|
+
* // Returns: { valid: true, errors: [], warnings: [] }
|
|
27
|
+
*/
|
|
28
|
+
async function validateDatasourceFile(filePath) {
|
|
29
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
30
|
+
throw new Error('File path is required and must be a string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
throw new Error(`File not found: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
let parsed;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(content);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
errors: [`Invalid JSON syntax: ${error.message}`],
|
|
46
|
+
warnings: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const validate = loadExternalDataSourceSchema();
|
|
51
|
+
const valid = validate(parsed);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
valid,
|
|
55
|
+
errors: valid ? [] : formatValidationErrors(validate.errors),
|
|
56
|
+
warnings: []
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
validateDatasourceFile
|
|
62
|
+
};
|
|
63
|
+
|
package/lib/diff.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Comparison Utilities
|
|
3
|
+
*
|
|
4
|
+
* Compares two configuration files and identifies differences.
|
|
5
|
+
* Used for deployment pipeline validation and schema migration detection.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview File comparison utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const logger = require('./utils/logger');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Performs deep comparison of two objects
|
|
19
|
+
* Returns differences as structured result
|
|
20
|
+
*
|
|
21
|
+
* @function compareObjects
|
|
22
|
+
* @param {Object} obj1 - First object
|
|
23
|
+
* @param {Object} obj2 - Second object
|
|
24
|
+
* @param {string} [path=''] - Current path in object (for nested fields)
|
|
25
|
+
* @returns {Object} Comparison result with added, removed, changed fields
|
|
26
|
+
*/
|
|
27
|
+
function compareObjects(obj1, obj2, currentPath = '') {
|
|
28
|
+
const result = {
|
|
29
|
+
added: [],
|
|
30
|
+
removed: [],
|
|
31
|
+
changed: [],
|
|
32
|
+
identical: true
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const allKeys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
|
|
36
|
+
|
|
37
|
+
for (const key of allKeys) {
|
|
38
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
39
|
+
const val1 = obj1 && obj1[key];
|
|
40
|
+
const val2 = obj2 && obj2[key];
|
|
41
|
+
|
|
42
|
+
if (!(key in obj1)) {
|
|
43
|
+
result.added.push({
|
|
44
|
+
path: newPath,
|
|
45
|
+
value: val2,
|
|
46
|
+
type: typeof val2
|
|
47
|
+
});
|
|
48
|
+
result.identical = false;
|
|
49
|
+
} else if (!(key in obj2)) {
|
|
50
|
+
result.removed.push({
|
|
51
|
+
path: newPath,
|
|
52
|
+
value: val1,
|
|
53
|
+
type: typeof val1
|
|
54
|
+
});
|
|
55
|
+
result.identical = false;
|
|
56
|
+
} else if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null && !Array.isArray(val1) && !Array.isArray(val2)) {
|
|
57
|
+
// Recursively compare nested objects
|
|
58
|
+
const nestedResult = compareObjects(val1, val2, newPath);
|
|
59
|
+
result.added.push(...nestedResult.added);
|
|
60
|
+
result.removed.push(...nestedResult.removed);
|
|
61
|
+
result.changed.push(...nestedResult.changed);
|
|
62
|
+
if (!nestedResult.identical) {
|
|
63
|
+
result.identical = false;
|
|
64
|
+
}
|
|
65
|
+
} else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
66
|
+
result.changed.push({
|
|
67
|
+
path: newPath,
|
|
68
|
+
oldValue: val1,
|
|
69
|
+
newValue: val2,
|
|
70
|
+
oldType: typeof val1,
|
|
71
|
+
newType: typeof val2
|
|
72
|
+
});
|
|
73
|
+
result.identical = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Identifies breaking changes in comparison result
|
|
82
|
+
* Breaking changes include: removed required fields, type changes
|
|
83
|
+
*
|
|
84
|
+
* @function identifyBreakingChanges
|
|
85
|
+
* @param {Object} comparison - Comparison result from compareObjects
|
|
86
|
+
* @param {Object} schema1 - First file schema (optional, for required fields check)
|
|
87
|
+
* @param {Object} schema2 - Second file schema (optional, for required fields check)
|
|
88
|
+
* @returns {Array} Array of breaking change descriptions
|
|
89
|
+
*/
|
|
90
|
+
function identifyBreakingChanges(comparison) {
|
|
91
|
+
const breaking = [];
|
|
92
|
+
|
|
93
|
+
// Removed fields are potentially breaking
|
|
94
|
+
comparison.removed.forEach(removed => {
|
|
95
|
+
breaking.push({
|
|
96
|
+
type: 'removed_field',
|
|
97
|
+
path: removed.path,
|
|
98
|
+
description: `Field removed: ${removed.path} (${removed.type})`
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Type changes are breaking
|
|
103
|
+
comparison.changed.forEach(change => {
|
|
104
|
+
if (change.oldType !== change.newType) {
|
|
105
|
+
breaking.push({
|
|
106
|
+
type: 'type_change',
|
|
107
|
+
path: change.path,
|
|
108
|
+
description: `Type changed: ${change.path} (${change.oldType} → ${change.newType})`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return breaking;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compares two configuration files
|
|
118
|
+
* Loads files, parses JSON, and performs deep comparison
|
|
119
|
+
*
|
|
120
|
+
* @async
|
|
121
|
+
* @function compareFiles
|
|
122
|
+
* @param {string} file1 - Path to first file
|
|
123
|
+
* @param {string} file2 - Path to second file
|
|
124
|
+
* @returns {Promise<Object>} Comparison result with differences
|
|
125
|
+
* @throws {Error} If files cannot be read or parsed
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* const result = await compareFiles('./old.json', './new.json');
|
|
129
|
+
* // Returns: { identical: false, added: [...], removed: [...], changed: [...] }
|
|
130
|
+
*/
|
|
131
|
+
async function compareFiles(file1, file2) {
|
|
132
|
+
if (!file1 || typeof file1 !== 'string') {
|
|
133
|
+
throw new Error('First file path is required');
|
|
134
|
+
}
|
|
135
|
+
if (!file2 || typeof file2 !== 'string') {
|
|
136
|
+
throw new Error('Second file path is required');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate files exist
|
|
140
|
+
if (!fs.existsSync(file1)) {
|
|
141
|
+
throw new Error(`File not found: ${file1}`);
|
|
142
|
+
}
|
|
143
|
+
if (!fs.existsSync(file2)) {
|
|
144
|
+
throw new Error(`File not found: ${file2}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read and parse files
|
|
148
|
+
let content1, content2;
|
|
149
|
+
let parsed1, parsed2;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
content1 = fs.readFileSync(file1, 'utf8');
|
|
153
|
+
parsed1 = JSON.parse(content1);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Failed to parse ${file1}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
content2 = fs.readFileSync(file2, 'utf8');
|
|
160
|
+
parsed2 = JSON.parse(content2);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to parse ${file2}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Compare objects
|
|
166
|
+
const comparison = compareObjects(parsed1, parsed2);
|
|
167
|
+
|
|
168
|
+
// Check for version changes
|
|
169
|
+
const version1 = parsed1.version || parsed1.metadata?.version || 'unknown';
|
|
170
|
+
const version2 = parsed2.version || parsed2.metadata?.version || 'unknown';
|
|
171
|
+
const versionChanged = version1 !== version2;
|
|
172
|
+
|
|
173
|
+
// Identify breaking changes
|
|
174
|
+
const breakingChanges = identifyBreakingChanges(comparison);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
identical: comparison.identical && !versionChanged,
|
|
178
|
+
file1: path.basename(file1),
|
|
179
|
+
file2: path.basename(file2),
|
|
180
|
+
version1,
|
|
181
|
+
version2,
|
|
182
|
+
versionChanged,
|
|
183
|
+
added: comparison.added,
|
|
184
|
+
removed: comparison.removed,
|
|
185
|
+
changed: comparison.changed,
|
|
186
|
+
breakingChanges,
|
|
187
|
+
summary: {
|
|
188
|
+
totalAdded: comparison.added.length,
|
|
189
|
+
totalRemoved: comparison.removed.length,
|
|
190
|
+
totalChanged: comparison.changed.length,
|
|
191
|
+
totalBreaking: breakingChanges.length
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Formats and displays diff output
|
|
198
|
+
* Shows differences in a user-friendly format with color coding
|
|
199
|
+
*
|
|
200
|
+
* @function formatDiffOutput
|
|
201
|
+
* @param {Object} diffResult - Comparison result from compareFiles
|
|
202
|
+
*/
|
|
203
|
+
function formatDiffOutput(diffResult) {
|
|
204
|
+
logger.log(chalk.blue(`\nComparing: ${diffResult.file1} ↔ ${diffResult.file2}`));
|
|
205
|
+
|
|
206
|
+
if (diffResult.identical) {
|
|
207
|
+
logger.log(chalk.green('\n✓ Files are identical'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
logger.log(chalk.yellow('\nFiles are different'));
|
|
212
|
+
|
|
213
|
+
// Version information
|
|
214
|
+
if (diffResult.versionChanged) {
|
|
215
|
+
logger.log(chalk.blue(`\nVersion: ${diffResult.version1} → ${diffResult.version2}`));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Breaking changes
|
|
219
|
+
if (diffResult.breakingChanges.length > 0) {
|
|
220
|
+
logger.log(chalk.red('\n⚠️ Breaking Changes:'));
|
|
221
|
+
diffResult.breakingChanges.forEach(change => {
|
|
222
|
+
logger.log(chalk.red(` • ${change.description}`));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Added fields
|
|
227
|
+
if (diffResult.added.length > 0) {
|
|
228
|
+
logger.log(chalk.green('\nAdded Fields:'));
|
|
229
|
+
diffResult.added.forEach(field => {
|
|
230
|
+
logger.log(chalk.green(` + ${field.path}: ${JSON.stringify(field.value)}`));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Removed fields
|
|
235
|
+
if (diffResult.removed.length > 0) {
|
|
236
|
+
logger.log(chalk.red('\nRemoved Fields:'));
|
|
237
|
+
diffResult.removed.forEach(field => {
|
|
238
|
+
logger.log(chalk.red(` - ${field.path}: ${JSON.stringify(field.value)}`));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Changed fields
|
|
243
|
+
if (diffResult.changed.length > 0) {
|
|
244
|
+
logger.log(chalk.yellow('\nChanged Fields:'));
|
|
245
|
+
diffResult.changed.forEach(change => {
|
|
246
|
+
logger.log(chalk.yellow(` ~ ${change.path}:`));
|
|
247
|
+
logger.log(chalk.gray(` Old: ${JSON.stringify(change.oldValue)}`));
|
|
248
|
+
logger.log(chalk.gray(` New: ${JSON.stringify(change.newValue)}`));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Summary
|
|
253
|
+
logger.log(chalk.blue('\nSummary:'));
|
|
254
|
+
logger.log(chalk.blue(` Added: ${diffResult.summary.totalAdded}`));
|
|
255
|
+
logger.log(chalk.blue(` Removed: ${diffResult.summary.totalRemoved}`));
|
|
256
|
+
logger.log(chalk.blue(` Changed: ${diffResult.summary.totalChanged}`));
|
|
257
|
+
logger.log(chalk.blue(` Breaking: ${diffResult.summary.totalBreaking}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
compareFiles,
|
|
262
|
+
formatDiffOutput,
|
|
263
|
+
compareObjects,
|
|
264
|
+
identifyBreakingChanges
|
|
265
|
+
};
|
|
266
|
+
|