@aifabrix/builder 2.7.0 → 2.9.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 (47) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/integration/hubspot/README.md +136 -0
  3. package/integration/hubspot/env.template +9 -0
  4. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  5. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  6. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  7. package/integration/hubspot/hubspot-deploy.json +91 -0
  8. package/integration/hubspot/variables.yaml +17 -0
  9. package/lib/app-config.js +13 -2
  10. package/lib/app-deploy.js +9 -3
  11. package/lib/app-dockerfile.js +14 -1
  12. package/lib/app-prompts.js +177 -13
  13. package/lib/app-push.js +16 -1
  14. package/lib/app-register.js +37 -5
  15. package/lib/app-rotate-secret.js +10 -0
  16. package/lib/app-run.js +19 -0
  17. package/lib/app.js +70 -25
  18. package/lib/audit-logger.js +9 -4
  19. package/lib/build.js +25 -13
  20. package/lib/cli.js +109 -2
  21. package/lib/commands/login.js +40 -3
  22. package/lib/config.js +121 -114
  23. package/lib/datasource-deploy.js +14 -20
  24. package/lib/environment-deploy.js +305 -0
  25. package/lib/external-system-deploy.js +345 -0
  26. package/lib/external-system-download.js +431 -0
  27. package/lib/external-system-generator.js +190 -0
  28. package/lib/external-system-test.js +446 -0
  29. package/lib/generator-builders.js +323 -0
  30. package/lib/generator.js +200 -292
  31. package/lib/schema/application-schema.json +830 -800
  32. package/lib/schema/external-datasource.schema.json +868 -46
  33. package/lib/schema/external-system.schema.json +98 -80
  34. package/lib/schema/infrastructure-schema.json +1 -1
  35. package/lib/templates.js +32 -1
  36. package/lib/utils/cli-utils.js +4 -4
  37. package/lib/utils/device-code.js +10 -2
  38. package/lib/utils/external-system-display.js +159 -0
  39. package/lib/utils/external-system-validators.js +245 -0
  40. package/lib/utils/paths.js +151 -1
  41. package/lib/utils/schema-resolver.js +7 -2
  42. package/lib/utils/token-encryption.js +68 -0
  43. package/lib/validator.js +52 -5
  44. package/package.json +1 -1
  45. package/tatus +181 -0
  46. package/templates/external-system/external-datasource.json.hbs +55 -0
  47. package/templates/external-system/external-system.json.hbs +37 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * External System Validation Helpers
3
+ *
4
+ * Provides validation functions for external system field mappings and schemas.
5
+ *
6
+ * @fileoverview Validation helpers for external system testing
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const Ajv = require('ajv');
12
+
13
+ /**
14
+ * Validates field mapping expression syntax (pipe-based DSL)
15
+ * @param {string} expression - Field mapping expression
16
+ * @returns {Object} Validation result with isValid and error message
17
+ */
18
+ function validateFieldMappingExpression(expression) {
19
+ if (!expression || typeof expression !== 'string') {
20
+ return { isValid: false, error: 'Expression must be a non-empty string' };
21
+ }
22
+
23
+ // Pattern: {{path.to.field}} | transformation1 | transformation2
24
+ const expressionPattern = /^\s*\{\{[^}]+\}\}(\s*\|\s*[a-zA-Z0-9_]+(\s*\([^)]*\))?)*\s*$/;
25
+
26
+ if (!expressionPattern.test(expression)) {
27
+ return {
28
+ isValid: false,
29
+ error: 'Invalid expression format. Expected: {{path.to.field}} | toUpper | trim'
30
+ };
31
+ }
32
+
33
+ // Extract path and transformations
34
+ const pathMatch = expression.match(/\{\{([^}]+)\}\}/);
35
+ if (!pathMatch) {
36
+ return { isValid: false, error: 'Path must be wrapped in {{}}' };
37
+ }
38
+
39
+ // Validate transformations (optional)
40
+ const transformations = expression.split('|').slice(1).map(t => t.trim());
41
+ const validTransformations = ['toUpper', 'toLower', 'trim', 'default', 'toNumber', 'toString'];
42
+ for (const trans of transformations) {
43
+ const transName = trans.split('(')[0].trim();
44
+ if (!validTransformations.includes(transName)) {
45
+ return {
46
+ isValid: false,
47
+ error: `Unknown transformation: ${transName}. Valid: ${validTransformations.join(', ')}`
48
+ };
49
+ }
50
+ }
51
+
52
+ return { isValid: true, error: null };
53
+ }
54
+
55
+ /**
56
+ * Validates field mappings against test payload
57
+ * @param {Object} datasource - Datasource configuration
58
+ * @param {Object} testPayload - Test payload object
59
+ * @returns {Object} Validation results
60
+ */
61
+ function validateFieldMappings(datasource, testPayload) {
62
+ const results = {
63
+ valid: true,
64
+ errors: [],
65
+ warnings: [],
66
+ mappedFields: {}
67
+ };
68
+
69
+ if (!datasource.fieldMappings || !datasource.fieldMappings.fields) {
70
+ results.warnings.push('No field mappings defined');
71
+ return results;
72
+ }
73
+
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
+ }
92
+
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
+ }
110
+
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
+ }
117
+ }
118
+
119
+ return results;
120
+ }
121
+
122
+ /**
123
+ * Validates metadata schema against test payload
124
+ * @param {Object} datasource - Datasource configuration
125
+ * @param {Object} testPayload - Test payload object
126
+ * @returns {Object} Validation results
127
+ */
128
+ function validateMetadataSchema(datasource, testPayload) {
129
+ const results = {
130
+ valid: true,
131
+ errors: [],
132
+ warnings: []
133
+ };
134
+
135
+ if (!datasource.metadataSchema) {
136
+ results.warnings.push('No metadata schema defined');
137
+ return results;
138
+ }
139
+
140
+ const payloadTemplate = testPayload.payloadTemplate || testPayload;
141
+
142
+ try {
143
+ const ajv = new Ajv({ allErrors: true, strict: false });
144
+ const validate = ajv.compile(datasource.metadataSchema);
145
+ const valid = validate(payloadTemplate);
146
+
147
+ if (!valid) {
148
+ results.valid = false;
149
+ results.errors = validate.errors.map(err => {
150
+ const path = err.instancePath || err.schemaPath;
151
+ return `${path} ${err.message}`;
152
+ });
153
+ }
154
+ } catch (error) {
155
+ results.valid = false;
156
+ results.errors.push(`Schema validation error: ${error.message}`);
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * Normalizes schema to handle nullable without type
164
+ * @param {Object} schema - Schema to normalize
165
+ * @returns {Object} Normalized schema
166
+ */
167
+ function normalizeSchema(schema) {
168
+ if (typeof schema !== 'object' || schema === null) {
169
+ return schema;
170
+ }
171
+
172
+ const normalized = Array.isArray(schema) ? [...schema] : { ...schema };
173
+
174
+ if (normalized.nullable === true && !normalized.type) {
175
+ normalized.type = ['null', 'string', 'number', 'boolean', 'object', 'array'];
176
+ }
177
+
178
+ for (const key in normalized) {
179
+ if (typeof normalized[key] === 'object' && normalized[key] !== null) {
180
+ normalized[key] = normalizeSchema(normalized[key]);
181
+ }
182
+ }
183
+
184
+ return normalized;
185
+ }
186
+
187
+ /**
188
+ * Validates JSON against schema
189
+ * @param {Object} data - Data to validate
190
+ * @param {Object} schema - JSON schema
191
+ * @returns {Object} Validation results
192
+ */
193
+ function validateAgainstSchema(data, schema) {
194
+ const ajv = new Ajv({
195
+ allErrors: true,
196
+ strict: false,
197
+ allowUnionTypes: true,
198
+ validateSchema: false
199
+ });
200
+ // Remove $schema for draft-2020-12 to avoid AJV issues
201
+ const schemaCopy = { ...schema };
202
+ if (schemaCopy.$schema && schemaCopy.$schema.includes('2020-12')) {
203
+ delete schemaCopy.$schema;
204
+ }
205
+ // Normalize schema to handle nullable without type
206
+ const normalizedSchema = normalizeSchema(schemaCopy);
207
+ const validate = ajv.compile(normalizedSchema);
208
+ const valid = validate(data);
209
+
210
+ if (!valid) {
211
+ // Filter out additionalProperties errors for required properties that aren't defined in schema
212
+ // This handles schema inconsistencies where authentication is required but not defined in properties
213
+ const filteredErrors = validate.errors.filter(err => {
214
+ if (err.keyword === 'additionalProperties' && err.params?.additionalProperty === 'authentication') {
215
+ // Check if authentication is in required array
216
+ const required = normalizedSchema.required || [];
217
+ if (required.includes('authentication')) {
218
+ return false; // Ignore this error since authentication is required but not defined
219
+ }
220
+ }
221
+ return true;
222
+ });
223
+
224
+ return {
225
+ valid: filteredErrors.length === 0,
226
+ errors: filteredErrors.map(err => {
227
+ const path = err.instancePath || err.schemaPath;
228
+ return `${path} ${err.message}`;
229
+ })
230
+ };
231
+ }
232
+
233
+ return {
234
+ valid: true,
235
+ errors: []
236
+ };
237
+ }
238
+
239
+ module.exports = {
240
+ validateFieldMappingExpression,
241
+ validateFieldMappings,
242
+ validateMetadataSchema,
243
+ validateAgainstSchema
244
+ };
245
+
@@ -91,9 +91,159 @@ function getDevDirectory(appName, developerId) {
91
91
  return baseDir;
92
92
  }
93
93
 
94
+ /**
95
+ * Gets the application path (builder or integration folder)
96
+ * @param {string} appName - Application name
97
+ * @param {string} [appType] - Application type ('external' or other)
98
+ * @returns {string} Absolute path to application directory
99
+ */
100
+ function getAppPath(appName, appType) {
101
+ if (!appName || typeof appName !== 'string') {
102
+ throw new Error('App name is required and must be a string');
103
+ }
104
+
105
+ const baseDir = appType === 'external' ? 'integration' : 'builder';
106
+ return path.join(process.cwd(), baseDir, appName);
107
+ }
108
+
109
+ /**
110
+ * Gets the integration folder path for external systems
111
+ * @param {string} appName - Application name
112
+ * @returns {string} Absolute path to integration directory
113
+ */
114
+ function getIntegrationPath(appName) {
115
+ if (!appName || typeof appName !== 'string') {
116
+ throw new Error('App name is required and must be a string');
117
+ }
118
+ return path.join(process.cwd(), 'integration', appName);
119
+ }
120
+
121
+ /**
122
+ * Gets the builder folder path for regular applications
123
+ * @param {string} appName - Application name
124
+ * @returns {string} Absolute path to builder directory
125
+ */
126
+ function getBuilderPath(appName) {
127
+ if (!appName || typeof appName !== 'string') {
128
+ throw new Error('App name is required and must be a string');
129
+ }
130
+ return path.join(process.cwd(), 'builder', appName);
131
+ }
132
+
133
+ /**
134
+ * Resolves the deployment JSON file path for an application
135
+ * Uses consistent naming: <app-name>-deploy.json
136
+ * Supports backward compatibility with aifabrix-deploy.json
137
+ *
138
+ * @param {string} appName - Application name
139
+ * @param {string} [appType] - Application type ('external' or other)
140
+ * @param {boolean} [preferNew] - If true, only return new naming (no backward compat)
141
+ * @returns {string} Absolute path to deployment JSON file
142
+ */
143
+ function getDeployJsonPath(appName, appType, preferNew = false) {
144
+ if (!appName || typeof appName !== 'string') {
145
+ throw new Error('App name is required and must be a string');
146
+ }
147
+
148
+ const appPath = getAppPath(appName, appType);
149
+ const newPath = path.join(appPath, `${appName}-deploy.json`);
150
+
151
+ // If preferNew is true, always return new naming
152
+ if (preferNew) {
153
+ return newPath;
154
+ }
155
+
156
+ // Check if new naming exists, otherwise fall back to old naming for backward compatibility
157
+ const oldPath = path.join(appPath, 'aifabrix-deploy.json');
158
+ if (fs.existsSync(newPath)) {
159
+ return newPath;
160
+ }
161
+
162
+ // Fall back to old naming for backward compatibility
163
+ if (fs.existsSync(oldPath)) {
164
+ return oldPath;
165
+ }
166
+
167
+ // If neither exists, return new naming (for generation)
168
+ return newPath;
169
+ }
170
+
171
+ /**
172
+ * Detects if an app is external type by checking variables.yaml
173
+ * Checks both integration/ and builder/ folders for backward compatibility
174
+ *
175
+ * @param {string} appName - Application name
176
+ * @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
177
+ */
178
+ async function detectAppType(appName) {
179
+ if (!appName || typeof appName !== 'string') {
180
+ throw new Error('App name is required and must be a string');
181
+ }
182
+
183
+ // Check integration folder first (new structure)
184
+ const integrationPath = getIntegrationPath(appName);
185
+ const integrationVariablesPath = path.join(integrationPath, 'variables.yaml');
186
+
187
+ if (fs.existsSync(integrationVariablesPath)) {
188
+ try {
189
+ const content = fs.readFileSync(integrationVariablesPath, 'utf8');
190
+ const variables = yaml.load(content);
191
+ if (variables.app && variables.app.type === 'external') {
192
+ return {
193
+ isExternal: true,
194
+ appPath: integrationPath,
195
+ appType: 'external',
196
+ baseDir: 'integration'
197
+ };
198
+ }
199
+ } catch {
200
+ // Ignore errors, continue to check builder folder
201
+ }
202
+ }
203
+
204
+ // Check builder folder (backward compatibility)
205
+ const builderPath = getBuilderPath(appName);
206
+ const builderVariablesPath = path.join(builderPath, 'variables.yaml');
207
+
208
+ if (fs.existsSync(builderVariablesPath)) {
209
+ try {
210
+ const content = fs.readFileSync(builderVariablesPath, 'utf8');
211
+ const variables = yaml.load(content);
212
+ const isExternal = variables.app && variables.app.type === 'external';
213
+ return {
214
+ isExternal,
215
+ appPath: builderPath,
216
+ appType: isExternal ? 'external' : 'regular',
217
+ baseDir: 'builder'
218
+ };
219
+ } catch {
220
+ // If we can't read it, assume regular app in builder folder
221
+ return {
222
+ isExternal: false,
223
+ appPath: builderPath,
224
+ appType: 'regular',
225
+ baseDir: 'builder'
226
+ };
227
+ }
228
+ }
229
+
230
+ // Default to builder folder if neither exists
231
+ return {
232
+ isExternal: false,
233
+ appPath: builderPath,
234
+ appType: 'regular',
235
+ baseDir: 'builder'
236
+ };
237
+ }
238
+
94
239
  module.exports = {
95
240
  getAifabrixHome,
96
241
  getApplicationsBaseDir,
97
- getDevDirectory
242
+ getDevDirectory,
243
+ getAppPath,
244
+ getIntegrationPath,
245
+ getBuilderPath,
246
+ getDeployJsonPath,
247
+ detectAppType
98
248
  };
99
249
 
@@ -12,6 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
+ const { detectAppType } = require('./paths');
15
16
 
16
17
  /**
17
18
  * Resolves schemaBasePath from application variables.yaml
@@ -32,7 +33,9 @@ async function resolveSchemaBasePath(appName) {
32
33
  throw new Error('App name is required and must be a string');
33
34
  }
34
35
 
35
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
36
+ // Detect app type and get correct path (integration or builder)
37
+ const { appPath } = await detectAppType(appName);
38
+ const variablesPath = path.join(appPath, 'variables.yaml');
36
39
 
37
40
  if (!fs.existsSync(variablesPath)) {
38
41
  throw new Error(`variables.yaml not found: ${variablesPath}`);
@@ -104,7 +107,9 @@ async function resolveExternalFiles(appName) {
104
107
  throw new Error('App name is required and must be a string');
105
108
  }
106
109
 
107
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
110
+ // Detect app type and get correct path (integration or builder)
111
+ const { appPath } = await detectAppType(appName);
112
+ const variablesPath = path.join(appPath, 'variables.yaml');
108
113
 
109
114
  if (!fs.existsSync(variablesPath)) {
110
115
  throw new Error(`variables.yaml not found: ${variablesPath}`);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * AI Fabrix Builder Token Encryption Utilities
3
+ *
4
+ * This module provides encryption and decryption functions for authentication tokens
5
+ * using AES-256-GCM algorithm for ISO 27001 compliance.
6
+ * Reuses the same encryption infrastructure as secrets encryption.
7
+ *
8
+ * @fileoverview Token encryption utilities for AI Fabrix Builder
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const { encryptSecret, decryptSecret, isEncrypted } = require('./secrets-encryption');
14
+
15
+ /**
16
+ * Encrypts a token value using AES-256-GCM
17
+ * Returns encrypted value in format: secure://<iv>:<ciphertext>:<authTag>
18
+ * All components are base64 encoded
19
+ *
20
+ * @function encryptToken
21
+ * @param {string} value - Plaintext token value to encrypt
22
+ * @param {string} key - Encryption key (hex or base64, 32 bytes)
23
+ * @returns {string} Encrypted value with secure:// prefix
24
+ * @throws {Error} If encryption fails or key is invalid
25
+ *
26
+ * @example
27
+ * const encrypted = encryptToken('my-token', 'a1b2c3...');
28
+ * // Returns: 'secure://<iv>:<ciphertext>:<authTag>'
29
+ */
30
+ function encryptToken(value, key) {
31
+ return encryptSecret(value, key);
32
+ }
33
+
34
+ /**
35
+ * Decrypts an encrypted token value
36
+ * Handles secure:// prefixed values and extracts IV, ciphertext, and auth tag
37
+ *
38
+ * @function decryptToken
39
+ * @param {string} encryptedValue - Encrypted value with secure:// prefix
40
+ * @param {string} key - Encryption key (hex or base64, 32 bytes)
41
+ * @returns {string} Decrypted plaintext value
42
+ * @throws {Error} If decryption fails, key is invalid, or format is incorrect
43
+ *
44
+ * @example
45
+ * const decrypted = decryptToken('secure://<iv>:<ciphertext>:<authTag>', 'a1b2c3...');
46
+ * // Returns: 'my-token'
47
+ */
48
+ function decryptToken(encryptedValue, key) {
49
+ return decryptSecret(encryptedValue, key);
50
+ }
51
+
52
+ /**
53
+ * Checks if a token value is encrypted (starts with secure://)
54
+ *
55
+ * @function isTokenEncrypted
56
+ * @param {string} value - Value to check
57
+ * @returns {boolean} True if value is encrypted
58
+ */
59
+ function isTokenEncrypted(value) {
60
+ return isEncrypted(value);
61
+ }
62
+
63
+ module.exports = {
64
+ encryptToken,
65
+ decryptToken,
66
+ isTokenEncrypted
67
+ };
68
+
package/lib/validator.js CHANGED
@@ -14,9 +14,12 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const Ajv = require('ajv');
16
16
  const applicationSchema = require('./schema/application-schema.json');
17
+ const externalSystemSchema = require('./schema/external-system.schema.json');
18
+ const externalDataSourceSchema = require('./schema/external-datasource.schema.json');
17
19
  const { transformVariablesForValidation } = require('./utils/variable-transformer');
18
20
  const { checkEnvironment } = require('./utils/environment-checker');
19
21
  const { formatValidationErrors } = require('./utils/error-formatter');
22
+ const { detectAppType } = require('./utils/paths');
20
23
 
21
24
  /**
22
25
  * Validates variables.yaml file against application schema
@@ -37,7 +40,9 @@ async function validateVariables(appName) {
37
40
  throw new Error('App name is required and must be a string');
38
41
  }
39
42
 
40
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
43
+ // Detect app type and get correct path (integration or builder)
44
+ const { appPath } = await detectAppType(appName);
45
+ const variablesPath = path.join(appPath, 'variables.yaml');
41
46
 
42
47
  if (!fs.existsSync(variablesPath)) {
43
48
  throw new Error(`variables.yaml not found: ${variablesPath}`);
@@ -56,13 +61,45 @@ async function validateVariables(appName) {
56
61
  const transformed = transformVariablesForValidation(variables, appName);
57
62
 
58
63
  const ajv = new Ajv({ allErrors: true, strict: false });
64
+ // Register external schemas with their $id (GitHub raw URLs)
65
+ // Create copies to avoid modifying the original schemas
66
+ const externalSystemSchemaCopy = { ...externalSystemSchema };
67
+ const externalDataSourceSchemaCopy = { ...externalDataSourceSchema };
68
+ // Remove $schema for draft-2020-12 to avoid AJV issues
69
+ if (externalDataSourceSchemaCopy.$schema && externalDataSourceSchemaCopy.$schema.includes('2020-12')) {
70
+ delete externalDataSourceSchemaCopy.$schema;
71
+ }
72
+ ajv.addSchema(externalSystemSchemaCopy, externalSystemSchema.$id);
73
+ ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
59
74
  const validate = ajv.compile(applicationSchema);
60
75
  const valid = validate(transformed);
61
76
 
77
+ // Additional explicit validation for external type
78
+ const errors = valid ? [] : formatValidationErrors(validate.errors);
79
+ const warnings = [];
80
+
81
+ // If type is external, perform additional checks
82
+ if (variables.app && variables.app.type === 'external') {
83
+ // Check for externalIntegration block
84
+ if (!variables.externalIntegration) {
85
+ errors.push('externalIntegration block is required when app.type is "external"');
86
+ } else {
87
+ // Validate externalIntegration structure
88
+ if (!variables.externalIntegration.schemaBasePath) {
89
+ errors.push('externalIntegration.schemaBasePath is required');
90
+ }
91
+ if (!variables.externalIntegration.systems || !Array.isArray(variables.externalIntegration.systems) || variables.externalIntegration.systems.length === 0) {
92
+ errors.push('externalIntegration.systems must be a non-empty array');
93
+ }
94
+ // Note: dataSources can be empty, so we don't validate that here
95
+ // File existence is validated during build/deploy, not during schema validation
96
+ }
97
+ }
98
+
62
99
  return {
63
- valid,
64
- errors: valid ? [] : formatValidationErrors(validate.errors),
65
- warnings: []
100
+ valid: valid && errors.length === 0,
101
+ errors,
102
+ warnings
66
103
  };
67
104
  }
68
105
 
@@ -220,7 +257,7 @@ async function validateEnvTemplate(appName) {
220
257
 
221
258
  /**
222
259
  * Validates deployment JSON against application schema
223
- * Ensures generated aifabrix-deploy.json matches the schema structure
260
+ * Ensures generated <app-name>-deploy.json matches the schema structure
224
261
  *
225
262
  * @function validateDeploymentJson
226
263
  * @param {Object} deployment - Deployment JSON object to validate
@@ -239,6 +276,16 @@ function validateDeploymentJson(deployment) {
239
276
  }
240
277
 
241
278
  const ajv = new Ajv({ allErrors: true, strict: false });
279
+ // Register external schemas with their $id (GitHub raw URLs)
280
+ // Create copies to avoid modifying the original schemas
281
+ const externalSystemSchemaCopy = { ...externalSystemSchema };
282
+ const externalDataSourceSchemaCopy = { ...externalDataSourceSchema };
283
+ // Remove $schema for draft-2020-12 to avoid AJV issues
284
+ if (externalDataSourceSchemaCopy.$schema && externalDataSourceSchemaCopy.$schema.includes('2020-12')) {
285
+ delete externalDataSourceSchemaCopy.$schema;
286
+ }
287
+ ajv.addSchema(externalSystemSchemaCopy, externalSystemSchema.$id);
288
+ ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
242
289
  const validate = ajv.compile(applicationSchema);
243
290
  const valid = validate(deployment);
244
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {