@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
package/lib/generator.js CHANGED
@@ -12,25 +12,12 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
+ const Ajv = require('ajv');
15
16
  const _secrets = require('./secrets');
16
17
  const _keyGenerator = require('./key-generator');
17
18
  const _validator = require('./validator');
18
-
19
- /**
20
- * Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
21
- * @function sanitizeAuthType
22
- * @param {string} authType - Authentication type
23
- * @returns {string} Sanitized authentication type
24
- */
25
- function sanitizeAuthType(authType) {
26
- if (authType === 'keycloak') {
27
- return 'azure';
28
- }
29
- if (authType && !['azure', 'local', 'none'].includes(authType)) {
30
- return 'azure'; // Default to azure if invalid type
31
- }
32
- return authType;
33
- }
19
+ const builders = require('./generator-builders');
20
+ const { detectAppType, getDeployJsonPath } = require('./utils/paths');
34
21
 
35
22
  /**
36
23
  * Loads variables.yaml file
@@ -84,229 +71,61 @@ function loadRbac(rbacPath) {
84
71
  }
85
72
 
86
73
  /**
87
- * Filters configuration based on registry mode
88
- * When registryMode is "external", only DOCKER_REGISTRY_SERVER_* variables are allowed
89
- * @function filterConfigurationByRegistryMode
90
- * @param {Array} configuration - Environment configuration
91
- * @param {string} registryMode - Registry mode ('external' or 'internal')
92
- * @returns {Array} Filtered configuration
93
- */
94
- function filterConfigurationByRegistryMode(configuration, registryMode) {
95
- if (registryMode !== 'external') {
96
- return configuration;
97
- }
98
-
99
- const allowedDockerRegistryVars = [
100
- 'DOCKER_REGISTRY_SERVER_URL',
101
- 'DOCKER_REGISTRY_SERVER_USERNAME',
102
- 'DOCKER_REGISTRY_SERVER_PASSWORD'
103
- ];
104
- return configuration.filter(config => allowedDockerRegistryVars.includes(config.name));
105
- }
106
-
107
- /**
108
- * Builds base deployment structure
109
- * @function buildBaseDeployment
110
- * @param {string} appName - Application name
111
- * @param {Object} variables - Variables configuration
112
- * @param {Array} filteredConfiguration - Filtered environment configuration
113
- * @returns {Object} Base deployment structure
114
- */
115
- function buildBaseDeployment(appName, variables, filteredConfiguration) {
116
- const requires = variables.requires || {};
117
- return {
118
- key: variables.app?.key || appName,
119
- displayName: variables.app?.displayName || appName,
120
- description: variables.app?.description || '',
121
- type: variables.app?.type || 'webapp',
122
- image: buildImageReference(variables),
123
- registryMode: variables.image?.registryMode || 'external',
124
- port: variables.port || 3000,
125
- requiresDatabase: requires.database || false,
126
- requiresRedis: requires.redis || false,
127
- requiresStorage: requires.storage || false,
128
- databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
129
- configuration: filteredConfiguration
130
- };
131
- }
132
-
133
- /**
134
- * Builds authentication configuration from variables or RBAC
135
- * @function buildAuthenticationConfig
136
- * @param {Object} variables - Variables configuration
137
- * @param {Object|null} rbac - RBAC configuration
138
- * @returns {Object} Authentication configuration
139
- */
140
- function buildAuthenticationConfig(variables, rbac) {
141
- if (variables.authentication) {
142
- const auth = {
143
- enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
144
- };
145
-
146
- // When enableSSO is false, default type to 'none' and requiredRoles to []
147
- // When enableSSO is true, require type and requiredRoles
148
- // Sanitize auth type (e.g., map keycloak to azure)
149
- if (auth.enableSSO === false) {
150
- auth.type = sanitizeAuthType(variables.authentication.type || 'none');
151
- auth.requiredRoles = variables.authentication.requiredRoles || [];
152
- } else {
153
- auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
154
- auth.requiredRoles = variables.authentication.requiredRoles || [];
155
- }
156
-
157
- if (variables.authentication.endpoints) {
158
- auth.endpoints = variables.authentication.endpoints;
159
- }
160
- return auth;
161
- }
162
- return buildAuthentication(rbac);
163
- }
164
-
165
- /**
166
- * Validates and transforms repository configuration
167
- * @function validateRepositoryConfig
168
- * @param {Object} repository - Repository configuration
169
- * @returns {Object|null} Validated repository config or null
74
+ * Generates external system <app-name>-deploy.json by loading the system JSON file
75
+ * For external systems, the system JSON file is already created and we just need to reference it
76
+ * @async
77
+ * @function generateExternalSystemDeployJson
78
+ * @param {string} appName - Name of the application
79
+ * @param {string} appPath - Path to application directory (integration or builder)
80
+ * @returns {Promise<string>} Path to generated <app-name>-deploy.json file
81
+ * @throws {Error} If generation fails
170
82
  */
171
- function validateRepositoryConfig(repository) {
172
- if (!repository || (!repository.enabled && !repository.repositoryUrl)) {
173
- return null;
83
+ async function generateExternalSystemDeployJson(appName, appPath) {
84
+ if (!appName || typeof appName !== 'string') {
85
+ throw new Error('App name is required and must be a string');
174
86
  }
175
87
 
176
- if (repository.repositoryUrl && repository.repositoryUrl.trim()) {
177
- return {
178
- enabled: repository.enabled || false,
179
- repositoryUrl: repository.repositoryUrl
180
- };
181
- }
88
+ const variablesPath = path.join(appPath, 'variables.yaml');
89
+ const { parsed: variables } = loadVariables(variablesPath);
182
90
 
183
- if (repository.enabled) {
184
- return { enabled: true };
91
+ if (!variables.externalIntegration) {
92
+ throw new Error('externalIntegration block not found in variables.yaml');
185
93
  }
186
94
 
187
- return null;
188
- }
95
+ // For external systems, the system JSON file should be in the same folder
96
+ // Check if it already exists (should be <app-name>-deploy.json)
97
+ const deployJsonPath = getDeployJsonPath(appName, 'external', true);
98
+ const systemFileName = variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0
99
+ ? variables.externalIntegration.systems[0]
100
+ : `${appName}-deploy.json`;
189
101
 
190
- /**
191
- * Validates and transforms build fields
192
- * @function validateBuildFields
193
- * @param {Object} build - Build configuration
194
- * @returns {Object|null} Validated build config or null
195
- */
196
- function validateBuildFields(build) {
197
- if (!build) {
198
- return null;
199
- }
102
+ // Resolve system file path (schemaBasePath is usually './' for same folder)
103
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
104
+ const systemFilePath = path.isAbsolute(schemaBasePath)
105
+ ? path.join(schemaBasePath, systemFileName)
106
+ : path.join(appPath, schemaBasePath, systemFileName);
200
107
 
201
- const buildConfig = {};
202
- if (build.envOutputPath) {
203
- buildConfig.envOutputPath = build.envOutputPath;
108
+ // If system file doesn't exist, throw error (it should be created manually or via external-system-generator)
109
+ if (!fs.existsSync(systemFilePath)) {
110
+ throw new Error(`External system file not found: ${systemFilePath}. Please create it first.`);
204
111
  }
205
- if (build.dockerfile && build.dockerfile.trim()) {
206
- buildConfig.dockerfile = build.dockerfile;
207
- }
208
-
209
- return Object.keys(buildConfig).length > 0 ? buildConfig : null;
210
- }
211
112
 
212
- /**
213
- * Validates and transforms deployment fields
214
- * @function validateDeploymentFields
215
- * @param {Object} deployment - Deployment configuration
216
- * @returns {Object|null} Validated deployment config or null
217
- */
218
- function validateDeploymentFields(deployment) {
219
- if (!deployment) {
220
- return null;
221
- }
113
+ // Read the system JSON file
114
+ const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
115
+ const systemJson = JSON.parse(systemContent);
222
116
 
223
- const deploymentConfig = {};
224
- if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
225
- deploymentConfig.controllerUrl = deployment.controllerUrl;
226
- }
117
+ // Write it as <app-name>-deploy.json (consistent naming)
118
+ const jsonContent = JSON.stringify(systemJson, null, 2);
119
+ await fs.promises.writeFile(deployJsonPath, jsonContent, { mode: 0o644, encoding: 'utf8' });
227
120
 
228
- return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
229
- }
230
-
231
- /**
232
- * Adds optional fields to deployment manifest
233
- * @function buildOptionalFields
234
- * @param {Object} deployment - Deployment manifest
235
- * @param {Object} variables - Variables configuration
236
- * @param {Object|null} rbac - RBAC configuration
237
- * @returns {Object} Deployment manifest with optional fields
238
- */
239
- function buildOptionalFields(deployment, variables, rbac) {
240
- if (variables.healthCheck) {
241
- deployment.healthCheck = buildHealthCheck(variables);
242
- }
243
-
244
- deployment.authentication = buildAuthenticationConfig(variables, rbac);
245
-
246
- // Add roles and permissions (from variables.yaml or rbac.yaml)
247
- // Priority: variables.yaml > rbac.yaml
248
- if (variables.roles) {
249
- deployment.roles = variables.roles;
250
- } else if (rbac && rbac.roles) {
251
- deployment.roles = rbac.roles;
252
- }
253
-
254
- if (variables.permissions) {
255
- deployment.permissions = variables.permissions;
256
- } else if (rbac && rbac.permissions) {
257
- deployment.permissions = rbac.permissions;
258
- }
259
-
260
- const repository = validateRepositoryConfig(variables.repository);
261
- if (repository) {
262
- deployment.repository = repository;
263
- }
264
-
265
- const build = validateBuildFields(variables.build);
266
- if (build) {
267
- deployment.build = build;
268
- }
269
-
270
- const deploymentConfig = validateDeploymentFields(variables.deployment);
271
- if (deploymentConfig) {
272
- deployment.deployment = deploymentConfig;
273
- }
274
-
275
- if (variables.startupCommand) {
276
- deployment.startupCommand = variables.startupCommand;
277
- }
278
- if (variables.runtimeVersion) {
279
- deployment.runtimeVersion = variables.runtimeVersion;
280
- }
281
- if (variables.scaling) {
282
- deployment.scaling = variables.scaling;
283
- }
284
- if (variables.frontDoorRouting) {
285
- deployment.frontDoorRouting = variables.frontDoorRouting;
286
- }
287
-
288
- return deployment;
289
- }
290
-
291
- /**
292
- * Builds deployment manifest structure
293
- * @param {string} appName - Application name
294
- * @param {Object} variables - Variables configuration
295
- * @param {string} deploymentKey - Deployment key
296
- * @param {Array} configuration - Environment configuration
297
- * @param {Object|null} rbac - RBAC configuration
298
- * @returns {Object} Deployment manifest
299
- */
300
- function buildManifestStructure(appName, variables, deploymentKey, configuration, rbac) {
301
- const registryMode = variables.image?.registryMode || 'external';
302
- const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
303
- const deployment = buildBaseDeployment(appName, variables, filteredConfiguration);
304
- return buildOptionalFields(deployment, variables, rbac);
121
+ return deployJsonPath;
305
122
  }
306
123
 
307
124
  /**
308
125
  * Generates deployment JSON from application configuration files
309
- * Creates aifabrix-deploy.json for Miso Controller deployment
126
+ * Creates <app-name>-deploy.json for all apps (consistent naming)
127
+ * For external systems, loads the system JSON file
128
+ * For regular apps, generates deployment manifest from variables.yaml, env.template, rbac.yaml
310
129
  *
311
130
  * @async
312
131
  * @function generateDeployJson
@@ -316,18 +135,26 @@ function buildManifestStructure(appName, variables, deploymentKey, configuration
316
135
  *
317
136
  * @example
318
137
  * const jsonPath = await generateDeployJson('myapp');
319
- * // Returns: './builder/myapp/aifabrix-deploy.json'
138
+ * // Returns: './builder/myapp/myapp-deploy.json' or './integration/hubspot/hubspot-deploy.json'
320
139
  */
321
140
  async function generateDeployJson(appName) {
322
141
  if (!appName || typeof appName !== 'string') {
323
142
  throw new Error('App name is required and must be a string');
324
143
  }
325
144
 
326
- const builderPath = path.join(process.cwd(), 'builder', appName);
327
- const variablesPath = path.join(builderPath, 'variables.yaml');
328
- const templatePath = path.join(builderPath, 'env.template');
329
- const rbacPath = path.join(builderPath, 'rbac.yaml');
330
- const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
145
+ // Detect app type and get correct path (integration or builder)
146
+ const { isExternal, appPath, appType } = await detectAppType(appName);
147
+ const variablesPath = path.join(appPath, 'variables.yaml');
148
+
149
+ // Check if app type is external
150
+ if (isExternal) {
151
+ return await generateExternalSystemDeployJson(appName, appPath);
152
+ }
153
+
154
+ // Regular app: generate deployment manifest
155
+ const templatePath = path.join(appPath, 'env.template');
156
+ const rbacPath = path.join(appPath, 'rbac.yaml');
157
+ const jsonPath = getDeployJsonPath(appName, appType, true); // Use new naming
331
158
 
332
159
  // Load configuration files
333
160
  const { parsed: variables } = loadVariables(variablesPath);
@@ -338,7 +165,7 @@ async function generateDeployJson(appName) {
338
165
  const configuration = parseEnvironmentVariables(envTemplate);
339
166
 
340
167
  // Build deployment manifest WITHOUT deploymentKey initially
341
- const deployment = buildManifestStructure(appName, variables, null, configuration, rbac);
168
+ const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
342
169
 
343
170
  // Generate deploymentKey from the manifest object (excluding deploymentKey field)
344
171
  const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
@@ -411,89 +238,170 @@ function parseEnvironmentVariables(envTemplate) {
411
238
  return configuration;
412
239
  }
413
240
 
414
- function buildImageReference(variables) {
415
- const imageName = variables.image?.name || variables.app?.key || 'app';
416
- const registry = variables.image?.registry;
417
- const tag = variables.image?.tag || 'latest';
241
+ async function generateDeployJsonWithValidation(appName) {
242
+ const jsonPath = await generateDeployJson(appName);
243
+ const jsonContent = fs.readFileSync(jsonPath, 'utf8');
244
+ const deployment = JSON.parse(jsonContent);
418
245
 
419
- if (registry) {
420
- return `${registry}/${imageName}:${tag}`;
421
- }
246
+ // Detect if this is an external system
247
+ const { isExternal } = await detectAppType(appName);
422
248
 
423
- return `${imageName}:${tag}`;
424
- }
249
+ // For external systems, skip deployment JSON validation (they use external system JSON structure)
250
+ if (isExternal) {
251
+ return {
252
+ success: true,
253
+ path: jsonPath,
254
+ validation: { valid: true, errors: [], warnings: [] },
255
+ deployment
256
+ };
257
+ }
425
258
 
426
- function buildHealthCheck(variables) {
427
- const healthCheck = {
428
- path: variables.healthCheck?.path || '/health',
429
- interval: variables.healthCheck?.interval || 30
259
+ const validation = _validator.validateDeploymentJson(deployment);
260
+ return {
261
+ success: validation.valid,
262
+ path: jsonPath,
263
+ validation,
264
+ deployment
430
265
  };
266
+ }
431
267
 
432
- // Add optional probe fields if present
433
- if (variables.healthCheck?.probePath) {
434
- healthCheck.probePath = variables.healthCheck.probePath;
268
+ /**
269
+ * Generates application-schema.json structure for external systems
270
+ * Combines system and datasource JSONs into application-level deployment format
271
+ * @async
272
+ * @function generateExternalSystemApplicationSchema
273
+ * @param {string} appName - Application name
274
+ * @returns {Promise<Object>} Application schema object
275
+ * @throws {Error} If generation fails
276
+ */
277
+ async function generateExternalSystemApplicationSchema(appName) {
278
+ if (!appName || typeof appName !== 'string') {
279
+ throw new Error('App name is required and must be a string');
435
280
  }
436
- if (variables.healthCheck?.probeRequestType) {
437
- healthCheck.probeRequestType = variables.healthCheck.probeRequestType;
281
+
282
+ const { appPath } = await detectAppType(appName);
283
+ const variablesPath = path.join(appPath, 'variables.yaml');
284
+
285
+ // Load variables.yaml
286
+ const { parsed: variables } = loadVariables(variablesPath);
287
+
288
+ if (!variables.externalIntegration) {
289
+ throw new Error('externalIntegration block not found in variables.yaml');
438
290
  }
439
- if (variables.healthCheck?.probeProtocol) {
440
- healthCheck.probeProtocol = variables.healthCheck.probeProtocol;
291
+
292
+ // Load system file
293
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
294
+ const systemFiles = variables.externalIntegration.systems || [];
295
+
296
+ if (systemFiles.length === 0) {
297
+ throw new Error('No system files specified in externalIntegration.systems');
441
298
  }
442
- if (variables.healthCheck?.probeIntervalInSeconds) {
443
- healthCheck.probeIntervalInSeconds = variables.healthCheck.probeIntervalInSeconds;
299
+
300
+ const systemFileName = systemFiles[0];
301
+ const systemFilePath = path.isAbsolute(schemaBasePath)
302
+ ? path.join(schemaBasePath, systemFileName)
303
+ : path.join(appPath, schemaBasePath, systemFileName);
304
+
305
+ if (!fs.existsSync(systemFilePath)) {
306
+ throw new Error(`System file not found: ${systemFilePath}`);
444
307
  }
445
308
 
446
- return healthCheck;
447
- }
309
+ const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
310
+ const systemJson = JSON.parse(systemContent);
448
311
 
449
- function buildRequirements(variables) {
450
- const requires = variables.requires || {};
312
+ // Load datasource files
313
+ const datasourceFiles = variables.externalIntegration.dataSources || [];
314
+ const datasourceJsons = [];
451
315
 
452
- return {
453
- database: requires.database || false,
454
- databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
455
- redis: requires.redis || false,
456
- storage: requires.storage || false,
457
- storageSize: requires.storageSize || '1Gi'
458
- };
459
- }
316
+ for (const datasourceFile of datasourceFiles) {
317
+ const datasourcePath = path.isAbsolute(schemaBasePath)
318
+ ? path.join(schemaBasePath, datasourceFile)
319
+ : path.join(appPath, schemaBasePath, datasourceFile);
460
320
 
461
- function buildAuthentication(rbac) {
462
- if (!rbac) {
463
- return {
464
- type: 'none',
465
- enableSSO: false,
466
- requiredRoles: []
467
- };
321
+ if (!fs.existsSync(datasourcePath)) {
322
+ throw new Error(`Datasource file not found: ${datasourcePath}`);
323
+ }
324
+
325
+ const datasourceContent = await fs.promises.readFile(datasourcePath, 'utf8');
326
+ const datasourceJson = JSON.parse(datasourceContent);
327
+ datasourceJsons.push(datasourceJson);
468
328
  }
469
329
 
470
- return {
471
- type: 'azure', // Default to azure (enum: azure, local, none)
472
- enableSSO: true,
473
- requiredRoles: rbac.roles?.map(role => role.value) || []
330
+ // Build application-schema.json structure
331
+ const applicationSchema = {
332
+ version: variables.externalIntegration.version || '1.0.0',
333
+ application: systemJson,
334
+ dataSources: datasourceJsons
474
335
  };
475
- }
476
336
 
477
- async function generateDeployJsonWithValidation(appName) {
478
- const jsonPath = await generateDeployJson(appName);
479
- const jsonContent = fs.readFileSync(jsonPath, 'utf8');
480
- const deployment = JSON.parse(jsonContent);
481
- const validation = _validator.validateDeploymentJson(deployment);
482
- return {
483
- success: validation.valid,
484
- path: jsonPath,
485
- validation,
486
- deployment
487
- };
337
+ // Validate individual components against their schemas
338
+ const externalSystemSchema = require('./schema/external-system.schema.json');
339
+ const externalDatasourceSchema = require('./schema/external-datasource.schema.json');
340
+
341
+ // For draft-2020-12 schemas, remove $schema to avoid AJV issues (similar to schema-loader.js)
342
+ const datasourceSchemaToAdd = { ...externalDatasourceSchema };
343
+ if (datasourceSchemaToAdd.$schema && datasourceSchemaToAdd.$schema.includes('2020-12')) {
344
+ delete datasourceSchemaToAdd.$schema;
345
+ }
346
+
347
+ const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });
348
+
349
+ // Validate application (system) against external-system schema
350
+ const externalSystemSchemaId = externalSystemSchema.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-system.schema.json';
351
+ ajv.addSchema(externalSystemSchema, externalSystemSchemaId);
352
+ const validateSystem = ajv.compile(externalSystemSchema);
353
+ const systemValid = validateSystem(systemJson);
354
+
355
+ if (!systemValid) {
356
+ // Filter out additionalProperties errors for required properties that aren't defined in schema
357
+ // This handles schema inconsistencies where authentication is required but not defined in properties
358
+ const filteredErrors = validateSystem.errors.filter(err => {
359
+ if (err.keyword === 'additionalProperties' && err.params?.additionalProperty === 'authentication') {
360
+ // Check if authentication is in required array
361
+ const required = externalSystemSchema.required || [];
362
+ if (required.includes('authentication')) {
363
+ return false; // Ignore this error since authentication is required but not defined
364
+ }
365
+ }
366
+ return true;
367
+ });
368
+
369
+ if (filteredErrors.length > 0) {
370
+ const errors = filteredErrors.map(err => {
371
+ const path = err.instancePath || err.schemaPath;
372
+ return `${path} ${err.message}`;
373
+ }).join(', ');
374
+ throw new Error(`System JSON does not match external-system schema: ${errors}`);
375
+ }
376
+ }
377
+
378
+ // Validate each datasource against external-datasource schema
379
+ const externalDatasourceSchemaId = datasourceSchemaToAdd.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-datasource.schema.json';
380
+ ajv.addSchema(datasourceSchemaToAdd, externalDatasourceSchemaId);
381
+ const validateDatasource = ajv.compile(datasourceSchemaToAdd);
382
+
383
+ for (let i = 0; i < datasourceJsons.length; i++) {
384
+ const datasourceValid = validateDatasource(datasourceJsons[i]);
385
+ if (!datasourceValid) {
386
+ const errors = validateDatasource.errors.map(err => {
387
+ const path = err.instancePath || err.schemaPath;
388
+ return `${path} ${err.message}`;
389
+ }).join(', ');
390
+ throw new Error(`Datasource ${i + 1} (${datasourceJsons[i].key || 'unknown'}) does not match external-datasource schema: ${errors}`);
391
+ }
392
+ }
393
+
394
+ return applicationSchema;
488
395
  }
489
396
 
490
397
  module.exports = {
491
398
  generateDeployJson,
492
399
  generateDeployJsonWithValidation,
400
+ generateExternalSystemApplicationSchema,
493
401
  parseEnvironmentVariables,
494
- buildImageReference,
495
- buildHealthCheck,
496
- buildRequirements,
497
- buildAuthentication,
498
- buildAuthenticationConfig
402
+ buildImageReference: builders.buildImageReference,
403
+ buildHealthCheck: builders.buildHealthCheck,
404
+ buildRequirements: builders.buildRequirements,
405
+ buildAuthentication: builders.buildAuthentication,
406
+ buildAuthenticationConfig: builders.buildAuthenticationConfig
499
407
  };