@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.
@@ -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
+