@aifabrix/builder 2.31.0 → 2.32.1

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 (119) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +34 -3
  111. package/scripts/install-local.js +210 -0
  112. package/templates/external-system/deploy.ps1.hbs +34 -0
  113. package/templates/external-system/deploy.sh.hbs +34 -0
  114. package/templates/external-system/external-datasource.json.hbs +31 -12
  115. package/lib/app.js +0 -467
  116. package/lib/datasource-list.js +0 -141
  117. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  118. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  119. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -12,72 +12,210 @@ const chalk = require('chalk');
12
12
  const logger = require('./logger');
13
13
 
14
14
  /**
15
- * Displays formatted test results
16
- * @param {Object} results - Test results
17
- * @param {boolean} verbose - Show detailed output
15
+ * Displays system file results
16
+ * @function displaySystemResults
17
+ * @param {Object[]} systemResults - System file results
18
18
  */
19
- function displayTestResults(results, verbose = false) {
20
- logger.log(chalk.blue('\n📊 Test Results\n'));
21
-
22
- if (results.systemResults.length > 0) {
23
- logger.log(chalk.blue('System Files:'));
24
- for (const systemResult of results.systemResults) {
25
- if (systemResult.valid) {
26
- logger.log(chalk.green(` ✓ ${systemResult.file}`));
27
- } else {
28
- logger.log(chalk.red(` ✗ ${systemResult.file}`));
29
- }
19
+ function displaySystemResults(systemResults) {
20
+ if (systemResults.length === 0) {
21
+ return;
22
+ }
23
+ logger.log(chalk.blue('System Files:'));
24
+ for (const systemResult of systemResults) {
25
+ if (systemResult.valid) {
26
+ logger.log(chalk.green(` ✓ ${systemResult.file}`));
27
+ } else {
28
+ logger.log(chalk.red(` ✗ ${systemResult.file}`));
30
29
  }
31
30
  }
31
+ }
32
32
 
33
- if (results.datasourceResults.length > 0) {
34
- logger.log(chalk.blue('\nDatasource Files:'));
35
- for (const dsResult of results.datasourceResults) {
36
- if (dsResult.valid) {
37
- logger.log(chalk.green(` ✓ ${dsResult.key} (${dsResult.file})`));
38
- } else {
39
- logger.log(chalk.red(` ${dsResult.key} (${dsResult.file})`));
40
- if (verbose && dsResult.errors.length > 0) {
41
- dsResult.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
42
- }
43
- }
33
+ /**
34
+ * Displays verbose datasource details
35
+ * @function displayVerboseDatasourceDetails
36
+ * @param {Object} dsResult - Datasource result
37
+ */
38
+ function displayVerboseDatasourceDetails(dsResult) {
39
+ if (dsResult.warnings.length > 0) {
40
+ dsResult.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
41
+ }
42
+
43
+ if (dsResult.fieldMappingResults) {
44
+ const fm = dsResult.fieldMappingResults;
45
+ logger.log(chalk.gray(` Field mappings: ${Object.keys(fm.mappedFields || {}).length} fields`));
46
+ }
47
+
48
+ if (dsResult.metadataSchemaResults) {
49
+ const ms = dsResult.metadataSchemaResults;
50
+ const statusMsg = ms.valid ? ' Metadata schema: ✓ Valid' : ' Metadata schema: ✗ Invalid';
51
+ logger.log(ms.valid ? chalk.gray(statusMsg) : chalk.red(statusMsg));
52
+ }
53
+ }
44
54
 
45
- if (verbose) {
46
- if (dsResult.warnings.length > 0) {
47
- dsResult.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
48
- }
49
-
50
- if (dsResult.fieldMappingResults) {
51
- const fm = dsResult.fieldMappingResults;
52
- logger.log(chalk.gray(` Field mappings: ${Object.keys(fm.mappedFields || {}).length} fields`));
53
- }
54
-
55
- if (dsResult.metadataSchemaResults) {
56
- const ms = dsResult.metadataSchemaResults;
57
- const statusMsg = ms.valid ? ' Metadata schema: ✓ Valid' : ' Metadata schema: ✗ Invalid';
58
- logger.log(ms.valid ? chalk.gray(statusMsg) : chalk.red(statusMsg));
59
- }
55
+ /**
56
+ * Displays datasource file results
57
+ * @function displayDatasourceResults
58
+ * @param {Object[]} datasourceResults - Datasource file results
59
+ * @param {boolean} verbose - Show detailed output
60
+ */
61
+ function displayDatasourceResults(datasourceResults, verbose) {
62
+ if (datasourceResults.length === 0) {
63
+ return;
64
+ }
65
+ logger.log(chalk.blue('\nDatasource Files:'));
66
+ for (const dsResult of datasourceResults) {
67
+ if (dsResult.valid) {
68
+ logger.log(chalk.green(` ✓ ${dsResult.key} (${dsResult.file})`));
69
+ } else {
70
+ logger.log(chalk.red(` ✗ ${dsResult.key} (${dsResult.file})`));
71
+ if (verbose && dsResult.errors.length > 0) {
72
+ dsResult.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
60
73
  }
61
74
  }
75
+
76
+ if (verbose) {
77
+ displayVerboseDatasourceDetails(dsResult);
78
+ }
62
79
  }
80
+ }
63
81
 
64
- if (results.errors.length > 0) {
82
+ /**
83
+ * Displays errors and warnings
84
+ * @function displayErrorsAndWarnings
85
+ * @param {string[]} errors - Error messages
86
+ * @param {string[]} warnings - Warning messages
87
+ */
88
+ function displayErrorsAndWarnings(errors, warnings) {
89
+ if (errors.length > 0) {
65
90
  logger.log(chalk.red('\n❌ Errors:'));
66
- results.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
91
+ errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
67
92
  }
68
93
 
69
- if (results.warnings.length > 0) {
94
+ if (warnings.length > 0) {
70
95
  logger.log(chalk.yellow('\n⚠ Warnings:'));
71
- results.warnings.forEach(warn => logger.log(chalk.yellow(` - ${warn}`)));
96
+ warnings.forEach(warn => logger.log(chalk.yellow(` - ${warn}`)));
72
97
  }
98
+ }
73
99
 
74
- if (results.valid) {
100
+ /**
101
+ * Displays final test status
102
+ * @function displayFinalTestStatus
103
+ * @param {boolean} valid - Whether all tests passed
104
+ */
105
+ function displayFinalTestStatus(valid) {
106
+ if (valid) {
75
107
  logger.log(chalk.green('\n✅ All tests passed!'));
76
108
  } else {
77
109
  logger.log(chalk.red('\n❌ Some tests failed'));
78
110
  }
79
111
  }
80
112
 
113
+ /**
114
+ * Displays formatted test results
115
+ * @param {Object} results - Test results
116
+ * @param {boolean} verbose - Show detailed output
117
+ */
118
+ function displayTestResults(results, verbose = false) {
119
+ logger.log(chalk.blue('\n📊 Test Results\n'));
120
+
121
+ displaySystemResults(results.systemResults);
122
+ displayDatasourceResults(results.datasourceResults, verbose);
123
+ displayErrorsAndWarnings(results.errors, results.warnings);
124
+ displayFinalTestStatus(results.valid);
125
+ }
126
+
127
+ /**
128
+ * Displays validation results in verbose mode
129
+ * @function displayVerboseValidationResults
130
+ * @param {Object} vr - Validation results
131
+ */
132
+ function displayVerboseValidationResults(vr) {
133
+ if (vr.isValid) {
134
+ logger.log(chalk.gray(' Validation: ✓ Valid'));
135
+ } else {
136
+ logger.log(chalk.red(' Validation: ✗ Invalid'));
137
+ }
138
+ if (vr.errors && vr.errors.length > 0) {
139
+ vr.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
140
+ }
141
+ if (vr.warnings && vr.warnings.length > 0) {
142
+ vr.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Displays field mapping results in verbose mode
148
+ * @function displayVerboseFieldMappingResults
149
+ * @param {Object} fmr - Field mapping results
150
+ */
151
+ function displayVerboseFieldMappingResults(fmr) {
152
+ logger.log(chalk.gray(` Field mappings: ${fmr.mappingCount || 0} attributes`));
153
+ if (fmr.dimensions && Object.keys(fmr.dimensions).length > 0) {
154
+ const dimensionKeys = Object.keys(fmr.dimensions);
155
+ logger.log(chalk.gray(` Dimensions: ${dimensionKeys.join(', ')}`));
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Displays endpoint test results in verbose mode
161
+ * @function displayVerboseEndpointResults
162
+ * @param {Object} etr - Endpoint test results
163
+ */
164
+ function displayVerboseEndpointResults(etr) {
165
+ if (etr.endpointConfigured) {
166
+ logger.log(chalk.gray(' Endpoint: ✓ Configured'));
167
+ } else {
168
+ logger.log(chalk.gray(' Endpoint: Not configured'));
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Displays verbose integration test details
174
+ * @function displayVerboseIntegrationDetails
175
+ * @param {Object} dsResult - Datasource result
176
+ */
177
+ function displayVerboseIntegrationDetails(dsResult) {
178
+ if (!dsResult.validationResults) {
179
+ return;
180
+ }
181
+
182
+ displayVerboseValidationResults(dsResult.validationResults);
183
+
184
+ if (dsResult.fieldMappingResults) {
185
+ displayVerboseFieldMappingResults(dsResult.fieldMappingResults);
186
+ }
187
+
188
+ if (dsResult.endpointTestResults) {
189
+ displayVerboseEndpointResults(dsResult.endpointTestResults);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Displays a single datasource integration test result
195
+ * @function displayDatasourceIntegrationResult
196
+ * @param {Object} dsResult - Datasource result
197
+ * @param {boolean} verbose - Show detailed output
198
+ */
199
+ function displayDatasourceIntegrationResult(dsResult, verbose) {
200
+ if (dsResult.skipped) {
201
+ logger.log(chalk.yellow(` ⚠ ${dsResult.key}: ${dsResult.reason}`));
202
+ return;
203
+ }
204
+
205
+ if (dsResult.success) {
206
+ logger.log(chalk.green(` ✓ ${dsResult.key}`));
207
+ } else {
208
+ logger.log(chalk.red(` ✗ ${dsResult.key}`));
209
+ if (dsResult.error) {
210
+ logger.log(chalk.red(` Error: ${dsResult.error}`));
211
+ }
212
+ }
213
+
214
+ if (verbose) {
215
+ displayVerboseIntegrationDetails(dsResult);
216
+ }
217
+ }
218
+
81
219
  /**
82
220
  * Displays formatted integration test results
83
221
  * @param {Object} results - Integration test results
@@ -93,51 +231,7 @@ function displayIntegrationTestResults(results, verbose = false) {
93
231
  }
94
232
 
95
233
  for (const dsResult of results.datasourceResults) {
96
- if (dsResult.skipped) {
97
- logger.log(chalk.yellow(` ⚠ ${dsResult.key}: ${dsResult.reason}`));
98
- continue;
99
- }
100
-
101
- if (dsResult.success) {
102
- logger.log(chalk.green(` ✓ ${dsResult.key}`));
103
- } else {
104
- logger.log(chalk.red(` ✗ ${dsResult.key}`));
105
- if (dsResult.error) {
106
- logger.log(chalk.red(` Error: ${dsResult.error}`));
107
- }
108
- }
109
-
110
- if (verbose && dsResult.validationResults) {
111
- const vr = dsResult.validationResults;
112
- if (vr.isValid) {
113
- logger.log(chalk.gray(' Validation: ✓ Valid'));
114
- } else {
115
- logger.log(chalk.red(' Validation: ✗ Invalid'));
116
- }
117
- if (vr.errors && vr.errors.length > 0) {
118
- vr.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
119
- }
120
- if (vr.warnings && vr.warnings.length > 0) {
121
- vr.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
122
- }
123
-
124
- if (dsResult.fieldMappingResults) {
125
- const fmr = dsResult.fieldMappingResults;
126
- logger.log(chalk.gray(` Field mappings: ${fmr.mappingCount || 0} fields`));
127
- if (fmr.accessFields && fmr.accessFields.length > 0) {
128
- logger.log(chalk.gray(` Access fields: ${fmr.accessFields.join(', ')}`));
129
- }
130
- }
131
-
132
- if (dsResult.endpointTestResults) {
133
- const etr = dsResult.endpointTestResults;
134
- if (etr.endpointConfigured) {
135
- logger.log(chalk.gray(' Endpoint: ✓ Configured'));
136
- } else {
137
- logger.log(chalk.gray(' Endpoint: Not configured'));
138
- }
139
- }
140
- }
234
+ displayDatasourceIntegrationResult(dsResult, verbose);
141
235
  }
142
236
 
143
237
  if (results.success) {
@@ -58,6 +58,158 @@ function validateFieldMappingExpression(expression) {
58
58
  * @param {Object} testPayload - Test payload object
59
59
  * @returns {Object} Validation results
60
60
  */
61
+ /**
62
+ * Validates a single field mapping
63
+ * @function validateSingleFieldMapping
64
+ * @param {string} fieldName - Field name
65
+ * @param {Object} fieldConfig - Field configuration
66
+ * @param {Object} payloadTemplate - Payload template
67
+ * @param {Object} results - Results object to update
68
+ */
69
+ function validateSingleFieldMapping(fieldName, fieldConfig, payloadTemplate, results) {
70
+ if (!fieldConfig.expression) {
71
+ results.errors.push(`Field '${fieldName}' missing expression`);
72
+ results.valid = false;
73
+ return;
74
+ }
75
+
76
+ // Validate expression syntax
77
+ const exprValidation = validateFieldMappingExpression(fieldConfig.expression);
78
+ if (!exprValidation.isValid) {
79
+ results.errors.push(`Field '${fieldName}': ${exprValidation.error}`);
80
+ results.valid = false;
81
+ return;
82
+ }
83
+
84
+ // Try to extract path from expression
85
+ const pathMatch = fieldConfig.expression.match(/\{\{([^}]+)\}\}/);
86
+ if (pathMatch) {
87
+ const fieldPath = pathMatch[1].trim();
88
+ const pathExists = checkPathExistsInPayload(fieldPath, payloadTemplate);
89
+
90
+ if (!pathExists) {
91
+ results.warnings.push(`Field '${fieldName}': Path '${fieldPath}' may not exist in payload`);
92
+ } else {
93
+ results.mappedFields[fieldName] = fieldConfig.expression;
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Checks if path exists in payload
100
+ * @function checkPathExistsInPayload
101
+ * @param {string} fieldPath - Field path
102
+ * @param {Object} payloadTemplate - Payload template
103
+ * @returns {boolean} True if path exists
104
+ */
105
+ function checkPathExistsInPayload(fieldPath, payloadTemplate) {
106
+ const pathParts = fieldPath.split('.');
107
+ let current = payloadTemplate;
108
+
109
+ for (const part of pathParts) {
110
+ if (current && typeof current === 'object' && part in current) {
111
+ current = current[part];
112
+ } else {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Validates dimensions object structure and content
122
+ * @param {Object} dimensions - Dimensions object to validate
123
+ * @param {Object} results - Results object to update
124
+ */
125
+ function validateDimensions(dimensions, results) {
126
+ if (!dimensions) {
127
+ results.errors.push('fieldMappings.dimensions is required (dimensions-first model)');
128
+ results.valid = false;
129
+ return;
130
+ }
131
+
132
+ if (typeof dimensions !== 'object' || Array.isArray(dimensions)) {
133
+ results.errors.push('fieldMappings.dimensions must be an object mapping dimension keys to attribute paths');
134
+ results.valid = false;
135
+ return;
136
+ }
137
+
138
+ // Validate dimension keys and values
139
+ for (const [dimensionKey, attributePath] of Object.entries(dimensions)) {
140
+ if (!/^[a-zA-Z0-9_]+$/.test(dimensionKey)) {
141
+ results.errors.push(`Invalid dimension key '${dimensionKey}': must match pattern ^[a-zA-Z0-9_]+$`);
142
+ results.valid = false;
143
+ }
144
+ if (typeof attributePath !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(attributePath)) {
145
+ results.errors.push(`Invalid attribute path '${attributePath}' for dimension '${dimensionKey}': must match pattern ^[a-zA-Z0-9_.]+$`);
146
+ results.valid = false;
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Validates attributes object structure
153
+ * @param {Object} attributes - Attributes object to validate
154
+ * @param {Object} results - Results object to update
155
+ * @returns {Object|null} Attributes object if valid, null otherwise
156
+ */
157
+ function validateAttributesStructure(attributes, results) {
158
+ if (!attributes) {
159
+ results.errors.push('fieldMappings.attributes is required');
160
+ results.valid = false;
161
+ return null;
162
+ }
163
+
164
+ if (typeof attributes !== 'object' || Array.isArray(attributes)) {
165
+ results.errors.push('fieldMappings.attributes must be an object');
166
+ results.valid = false;
167
+ return null;
168
+ }
169
+
170
+ return attributes;
171
+ }
172
+
173
+ /**
174
+ * Validates a single attribute configuration
175
+ * @param {string} attributeName - Attribute name
176
+ * @param {Object} attributeConfig - Attribute configuration
177
+ * @param {Object} payloadTemplate - Payload template
178
+ * @param {Object} results - Results object to update
179
+ */
180
+ function validateSingleAttribute(attributeName, attributeConfig, payloadTemplate, results) {
181
+ if (!attributeConfig || typeof attributeConfig !== 'object') {
182
+ results.errors.push(`Attribute '${attributeName}' must be an object with expression and type`);
183
+ results.valid = false;
184
+ return;
185
+ }
186
+
187
+ // Validate indexed property if present
188
+ if ('indexed' in attributeConfig && typeof attributeConfig.indexed !== 'boolean') {
189
+ results.errors.push(`Attribute '${attributeName}': indexed property must be a boolean`);
190
+ results.valid = false;
191
+ }
192
+
193
+ // Validate expression
194
+ if (!attributeConfig.expression || typeof attributeConfig.expression !== 'string') {
195
+ results.errors.push(`Attribute '${attributeName}': expression is required`);
196
+ results.valid = false;
197
+ return;
198
+ }
199
+
200
+ // Validate record_ref: expressions
201
+ if (attributeConfig.expression.startsWith('record_ref:')) {
202
+ const entityType = attributeConfig.expression.substring(11);
203
+ if (!/^[a-z0-9-]+$/.test(entityType)) {
204
+ results.errors.push(`Attribute '${attributeName}': Invalid entity type in record_ref expression: ${entityType}`);
205
+ results.valid = false;
206
+ }
207
+ } else {
208
+ // Validate regular expression syntax
209
+ validateSingleFieldMapping(attributeName, attributeConfig, payloadTemplate, results);
210
+ }
211
+ }
212
+
61
213
  function validateFieldMappings(datasource, testPayload) {
62
214
  const results = {
63
215
  valid: true,
@@ -66,54 +218,24 @@ function validateFieldMappings(datasource, testPayload) {
66
218
  mappedFields: {}
67
219
  };
68
220
 
69
- if (!datasource.fieldMappings || !datasource.fieldMappings.fields) {
221
+ if (!datasource.fieldMappings) {
70
222
  results.warnings.push('No field mappings defined');
71
223
  return results;
72
224
  }
73
225
 
74
- const fields = datasource.fieldMappings.fields;
75
- const payloadTemplate = testPayload.payloadTemplate || testPayload;
76
-
77
- // Validate each field mapping expression
78
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
79
- if (!fieldConfig.expression) {
80
- results.errors.push(`Field '${fieldName}' missing expression`);
81
- results.valid = false;
82
- continue;
83
- }
84
-
85
- // Validate expression syntax
86
- const exprValidation = validateFieldMappingExpression(fieldConfig.expression);
87
- if (!exprValidation.isValid) {
88
- results.errors.push(`Field '${fieldName}': ${exprValidation.error}`);
89
- results.valid = false;
90
- continue;
91
- }
226
+ // Validate dimensions (required in new schema)
227
+ validateDimensions(datasource.fieldMappings.dimensions, results);
92
228
 
93
- // Try to extract path from expression
94
- const pathMatch = fieldConfig.expression.match(/\{\{([^}]+)\}\}/);
95
- if (pathMatch) {
96
- const fieldPath = pathMatch[1].trim();
97
- // Check if path exists in payload (simple check)
98
- const pathParts = fieldPath.split('.');
99
- let current = payloadTemplate;
100
- let pathExists = true;
101
-
102
- for (const part of pathParts) {
103
- if (current && typeof current === 'object' && part in current) {
104
- current = current[part];
105
- } else {
106
- pathExists = false;
107
- break;
108
- }
109
- }
229
+ // Validate attributes structure (required in new schema)
230
+ const attributes = validateAttributesStructure(datasource.fieldMappings.attributes, results);
231
+ if (!attributes) {
232
+ return results;
233
+ }
110
234
 
111
- if (!pathExists) {
112
- results.warnings.push(`Field '${fieldName}': Path '${fieldPath}' may not exist in payload`);
113
- } else {
114
- results.mappedFields[fieldName] = fieldConfig.expression;
115
- }
116
- }
235
+ // Validate each attribute
236
+ const payloadTemplate = testPayload.payloadTemplate || testPayload;
237
+ for (const [attributeName, attributeConfig] of Object.entries(attributes)) {
238
+ validateSingleAttribute(attributeName, attributeConfig, payloadTemplate, results);
117
239
  }
118
240
 
119
241
  return results;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @fileoverview File upload utilities for multipart/form-data requests
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const fs = require('fs').promises;
8
+ const path = require('path');
9
+ const { makeApiCall, authenticatedApiCall } = require('./api');
10
+
11
+ /**
12
+ * Upload a file using multipart/form-data
13
+ * @async
14
+ * @function uploadFile
15
+ * @param {string} url - API endpoint URL
16
+ * @param {string} filePath - Path to file to upload
17
+ * @param {string} fieldName - Form field name for the file (default: 'file')
18
+ * @param {Object} [authConfig] - Authentication configuration
19
+ * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-credentials')
20
+ * @param {string} [authConfig.token] - Bearer token
21
+ * @param {string} [authConfig.clientId] - Client ID
22
+ * @param {string} [authConfig.clientSecret] - Client secret
23
+ * @param {Object} [additionalFields] - Additional form fields to include
24
+ * @returns {Promise<Object>} API response
25
+ * @throws {Error} If file upload fails
26
+ */
27
+ /**
28
+ * Validates file exists
29
+ * @async
30
+ * @function validateFileExists
31
+ * @param {string} filePath - File path
32
+ * @throws {Error} If file not found
33
+ */
34
+ async function validateFileExists(filePath) {
35
+ try {
36
+ await fs.access(filePath);
37
+ } catch (error) {
38
+ throw new Error(`File not found: ${filePath}`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Builds FormData with file and additional fields
44
+ * @async
45
+ * @function buildFormData
46
+ * @param {string} filePath - File path
47
+ * @param {string} fieldName - Field name
48
+ * @param {Object} additionalFields - Additional fields
49
+ * @returns {Promise<FormData>} FormData object
50
+ */
51
+ async function buildFormData(filePath, fieldName, additionalFields) {
52
+ const formData = new FormData();
53
+ const fileContent = await fs.readFile(filePath);
54
+ const fileName = path.basename(filePath);
55
+ const fileBlob = new Blob([fileContent], { type: 'application/octet-stream' });
56
+ formData.append(fieldName, fileBlob, fileName);
57
+
58
+ for (const [key, value] of Object.entries(additionalFields)) {
59
+ formData.append(key, String(value));
60
+ }
61
+
62
+ return formData;
63
+ }
64
+
65
+ /**
66
+ * Builds authentication headers
67
+ * @function buildAuthHeaders
68
+ * @param {Object} authConfig - Authentication configuration
69
+ * @returns {Object} Headers object
70
+ */
71
+ function buildAuthHeaders(authConfig) {
72
+ const headers = {};
73
+ if (authConfig.type === 'bearer' && authConfig.token) {
74
+ headers['Authorization'] = `Bearer ${authConfig.token}`;
75
+ } else if (authConfig.type === 'client-credentials') {
76
+ if (authConfig.clientId) {
77
+ headers['x-client-id'] = authConfig.clientId;
78
+ }
79
+ if (authConfig.clientSecret) {
80
+ headers['x-client-secret'] = authConfig.clientSecret;
81
+ }
82
+ }
83
+ return headers;
84
+ }
85
+
86
+ async function uploadFile(url, filePath, fieldName = 'file', authConfig = {}, additionalFields = {}) {
87
+ await validateFileExists(filePath);
88
+
89
+ const formData = await buildFormData(filePath, fieldName, additionalFields);
90
+ const headers = buildAuthHeaders(authConfig);
91
+
92
+ const options = {
93
+ method: 'POST',
94
+ headers,
95
+ body: formData
96
+ };
97
+
98
+ // Use authenticatedApiCall if bearer token, otherwise makeApiCall
99
+ if (authConfig.type === 'bearer' && authConfig.token) {
100
+ return await authenticatedApiCall(url, options, authConfig.token);
101
+ }
102
+
103
+ return await makeApiCall(url, options);
104
+ }
105
+
106
+ module.exports = {
107
+ uploadFile
108
+ };
109
+