@aifabrix/builder 2.6.3 → 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,262 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://aifabrix.ai/schemas/external-system.schema.json",
4
+ "title": "AI Fabrix External System Configuration Schema",
5
+ "description": "Schema for configuring an external system connected to the AI Fabrix Dataplane. This defines authentication, OpenAPI/MCP bindings, field mappings defaults, metadata handling and portal inputs.",
6
+
7
+ "metadata": {
8
+ "key": "external-system-schema",
9
+ "name": "External System Configuration Schema",
10
+ "description": "JSON schema for validating ExternalSystem configuration files",
11
+ "version": "1.0.0",
12
+ "type": "schema",
13
+ "category": "integration",
14
+ "author": "AI Fabrix Team",
15
+ "createdAt": "2024-01-01T00:00:00Z",
16
+ "updatedAt": "2024-01-01T00:00:00Z",
17
+ "compatibility": {
18
+ "minVersion": "1.0.0",
19
+ "maxVersion": "2.0.0",
20
+ "deprecated": false
21
+ },
22
+ "tags": ["schema", "external-system", "dataplane", "integration", "validation"],
23
+ "dependencies": [],
24
+ "changelog": [
25
+ {
26
+ "version": "1.0.0",
27
+ "date": "2024-01-01T00:00:00Z",
28
+ "changes": [
29
+ "Initial schema for External Systems",
30
+ "Added OpenAPI and MCP contract configuration",
31
+ "Added authentication and environment variables",
32
+ "Added portalInput for UI-driven onboarding",
33
+ "Compatible with field-mappings.schema and security-model.schema"
34
+ ],
35
+ "breaking": false
36
+ }
37
+ ]
38
+ },
39
+
40
+ "type": "object",
41
+
42
+ "required": [
43
+ "key",
44
+ "displayName",
45
+ "description",
46
+ "type",
47
+ "authentication"
48
+ ],
49
+
50
+ "properties": {
51
+ "key": {
52
+ "type": "string",
53
+ "description": "Unique external system identifier (cannot be changed after creation). Example: 'hubspot', 'salesforce', 'teams'.",
54
+ "pattern": "^[a-z0-9-]+$",
55
+ "minLength": 3,
56
+ "maxLength": 40
57
+ },
58
+
59
+ "displayName": {
60
+ "type": "string",
61
+ "description": "Human-readable name for the external system",
62
+ "minLength": 1,
63
+ "maxLength": 100
64
+ },
65
+
66
+ "description": {
67
+ "type": "string",
68
+ "description": "Description of this external system integration",
69
+ "minLength": 1,
70
+ "maxLength": 500
71
+ },
72
+
73
+ "type": {
74
+ "type": "string",
75
+ "enum": ["openapi", "mcp", "custom"],
76
+ "description": "Integration type: OpenAPI-driven, MCP-driven, or custom Python connector."
77
+ },
78
+
79
+ "enabled": {
80
+ "type": "boolean",
81
+ "default": true
82
+ },
83
+
84
+ "environment": {
85
+ "type": "object",
86
+ "description": "Environment-level configuration values used by dataplane and external data sources.",
87
+ "properties": {
88
+ "baseUrl": {
89
+ "type": "string",
90
+ "description": "Base API URL or MCP server URL",
91
+ "pattern": "^(http|https)://.*$"
92
+ },
93
+ "region": {
94
+ "type": "string",
95
+ "description": "Optional region setting for API routing"
96
+ }
97
+ },
98
+ "additionalProperties": true
99
+ },
100
+
101
+ "authentication": {
102
+ "type": "object",
103
+ "description": "Authentication configuration for the external system.",
104
+ "required": ["mode"],
105
+ "properties": {
106
+ "mode": {
107
+ "type": "string",
108
+ "enum": ["oauth2", "apikey", "basic", "aad", "none"],
109
+ "description": "Authentication method used for API access."
110
+ },
111
+
112
+ "oauth2": {
113
+ "type": "object",
114
+ "description": "OAuth2 configuration used when mode == 'oauth2'. All secrets are stored in Key Vault.",
115
+ "properties": {
116
+ "tokenUrl": {
117
+ "type": "string",
118
+ "pattern": "^(http|https)://.*$"
119
+ },
120
+ "clientId": {
121
+ "type": "string"
122
+ },
123
+ "clientSecret": {
124
+ "type": "string"
125
+ },
126
+ "scopes": {
127
+ "type": "array",
128
+ "items": { "type": "string" }
129
+ }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+
134
+ "apikey": {
135
+ "type": "object",
136
+ "properties": {
137
+ "headerName": { "type": "string" },
138
+ "key": { "type": "string" }
139
+ },
140
+ "additionalProperties": false
141
+ },
142
+
143
+ "basic": {
144
+ "type": "object",
145
+ "properties": {
146
+ "username": { "type": "string" },
147
+ "password": { "type": "string" }
148
+ },
149
+ "additionalProperties": false
150
+ }
151
+ },
152
+ "additionalProperties": false
153
+ },
154
+
155
+ "openapi": {
156
+ "type": "object",
157
+ "description": "OpenAPI integration configuration.",
158
+ "properties": {
159
+ "documentKey": {
160
+ "type": "string",
161
+ "description": "Reference to OpenAPI spec registered via builder. Example: 'hubspot-v3'."
162
+ },
163
+ "autoDiscoverEntities": {
164
+ "type": "boolean",
165
+ "default": false,
166
+ "description": "Automatically discover resources/entities from OpenAPI schema."
167
+ }
168
+ },
169
+ "additionalProperties": false
170
+ },
171
+
172
+ "mcp": {
173
+ "type": "object",
174
+ "description": "Model Context Protocol integration config.",
175
+ "properties": {
176
+ "serverUrl": {
177
+ "type": "string",
178
+ "pattern": "^(http|https)://.*$"
179
+ },
180
+ "toolPrefix": {
181
+ "type": "string",
182
+ "pattern": "^[a-z0-9-]+$",
183
+ "description": "Prefix for MCP tool names generated from this system."
184
+ }
185
+ },
186
+ "additionalProperties": false
187
+ },
188
+
189
+ "dataSources": {
190
+ "type": "array",
191
+ "description": "List of data source keys belonging to this external system. Each must match external-datasource.schema.json.",
192
+ "items": {
193
+ "type": "string",
194
+ "pattern": "^[a-z0-9-]+$"
195
+ },
196
+ "uniqueItems": true
197
+ },
198
+
199
+ "configuration": {
200
+ "type": "array",
201
+ "description": "External system configuration variables (same pattern as application-schema).",
202
+ "items": {
203
+ "type": "object",
204
+ "required": ["name", "value", "location", "required"],
205
+ "properties": {
206
+ "name": {
207
+ "type": "string",
208
+ "pattern": "^[A-Z_][A-Z0-9_]*$"
209
+ },
210
+ "value": {
211
+ "type": "string",
212
+ "description": "Literal value or parameter reference ({{parameter}})"
213
+ },
214
+ "location": {
215
+ "type": "string",
216
+ "enum": ["variable", "keyvault"]
217
+ },
218
+ "required": {
219
+ "type": "boolean"
220
+ },
221
+ "portalInput": {
222
+ "type": "object",
223
+ "required": ["field", "label"],
224
+ "properties": {
225
+ "field": {
226
+ "type": "string",
227
+ "enum": ["password", "text", "textarea", "select", "json"]
228
+ },
229
+ "label": { "type": "string" },
230
+ "placeholder": { "type": "string" },
231
+ "options": {
232
+ "type": "array",
233
+ "items": { "type": "string" }
234
+ },
235
+ "masked": { "type": "boolean" },
236
+ "validation": {
237
+ "type": "object",
238
+ "properties": {
239
+ "minLength": { "type": "integer" },
240
+ "maxLength": { "type": "integer" },
241
+ "pattern": { "type": "string" },
242
+ "required": { "type": "boolean" }
243
+ },
244
+ "additionalProperties": false
245
+ }
246
+ },
247
+ "additionalProperties": false
248
+ }
249
+ },
250
+ "additionalProperties": false
251
+ }
252
+ },
253
+
254
+ "tags": {
255
+ "type": "array",
256
+ "description": "Optional tags for search / filtering",
257
+ "items": { "type": "string" }
258
+ }
259
+ },
260
+
261
+ "additionalProperties": false
262
+ }
package/lib/secrets.js CHANGED
@@ -20,7 +20,6 @@ const {
20
20
  formatMissingSecretsFileInfo,
21
21
  replaceKvInContent,
22
22
  loadEnvTemplate,
23
- processEnvVariables,
24
23
  adjustLocalEnvPortsInContent,
25
24
  rewriteInfraEndpoints,
26
25
  readYamlAtPath,
@@ -28,6 +27,7 @@ const {
28
27
  ensureNonEmptySecrets,
29
28
  validateSecrets
30
29
  } = require('./utils/secrets-helpers');
30
+ const { processEnvVariables } = require('./utils/env-copy');
31
31
  const { buildEnvVarMap } = require('./utils/env-map');
32
32
  const { resolveServicePortsInEnvContent } = require('./utils/secrets-url');
33
33
  const {
@@ -291,6 +291,25 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
291
291
  if (environment === 'docker') {
292
292
  resolved = await resolveServicePortsInEnvContent(resolved, environment);
293
293
  resolved = await rewriteInfraEndpoints(resolved, 'docker');
294
+ // Interpolate ${VAR} references created by rewriteInfraEndpoints
295
+ // Get the actual host and port values from env-endpoints.js directly
296
+ // to ensure they are correctly populated in envVars for interpolation
297
+ const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('./utils/env-endpoints');
298
+ const hosts = await getEnvHosts('docker');
299
+ const localhostOverride = getLocalhostOverride('docker');
300
+ const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);
301
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
302
+ const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
303
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
304
+
305
+ // Build envVars map and ensure it has the correct values
306
+ const envVars = await buildEnvVarMap('docker');
307
+ // Override with the actual values that were just set by rewriteInfraEndpoints
308
+ envVars.REDIS_HOST = redisHost;
309
+ envVars.REDIS_PORT = String(redisPort);
310
+ envVars.DB_HOST = dbHost;
311
+ envVars.DB_PORT = String(dbPort);
312
+ resolved = interpolateEnvVars(resolved, envVars);
294
313
  resolved = await updatePortForDocker(resolved, variablesPath);
295
314
  } else if (environment === 'local') {
296
315
  // adjustLocalEnvPortsInContent handles both PORT and infra endpoints
@@ -16,6 +16,8 @@ const logger = require('./logger');
16
16
  const config = require('../config');
17
17
  const devConfig = require('../utils/dev-config');
18
18
  const { rewriteInfraEndpoints } = require('./env-endpoints');
19
+ const { buildEnvVarMap } = require('./env-map');
20
+ const { interpolateEnvVars } = require('./secrets-helpers');
19
21
 
20
22
  /**
21
23
  * Process and optionally copy env file to envOutputPath if configured
@@ -120,6 +122,28 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
120
122
  });
121
123
  // Rewrite infra endpoints using env-config mapping for local context
122
124
  envContent = await rewriteInfraEndpoints(envContent, 'local', infraPorts);
125
+ // Interpolate ${VAR} references created by rewriteInfraEndpoints
126
+ // Extract actual values from updated content to use for interpolation
127
+ const envVars = await buildEnvVarMap('local', null, devIdNum);
128
+ // Extract REDIS_HOST, REDIS_PORT, DB_HOST, DB_PORT from updated content if present
129
+ // Only extract if the value doesn't contain ${VAR} references (to avoid circular interpolation)
130
+ const redisHostMatch = envContent.match(/^REDIS_HOST\s*=\s*([^\r\n$]+)/m);
131
+ const redisPortMatch = envContent.match(/^REDIS_PORT\s*=\s*([^\r\n$]+)/m);
132
+ const dbHostMatch = envContent.match(/^DB_HOST\s*=\s*([^\r\n$]+)/m);
133
+ const dbPortMatch = envContent.match(/^DB_PORT\s*=\s*([^\r\n$]+)/m);
134
+ if (redisHostMatch && redisHostMatch[1] && !redisHostMatch[1].includes('${')) {
135
+ envVars.REDIS_HOST = redisHostMatch[1].trim();
136
+ }
137
+ if (redisPortMatch && redisPortMatch[1] && !redisPortMatch[1].includes('${')) {
138
+ envVars.REDIS_PORT = redisPortMatch[1].trim();
139
+ }
140
+ if (dbHostMatch && dbHostMatch[1] && !dbHostMatch[1].includes('${')) {
141
+ envVars.DB_HOST = dbHostMatch[1].trim();
142
+ }
143
+ if (dbPortMatch && dbPortMatch[1] && !dbPortMatch[1].includes('${')) {
144
+ envVars.DB_PORT = dbPortMatch[1].trim();
145
+ }
146
+ envContent = interpolateEnvVars(envContent, envVars);
123
147
  fs.writeFileSync(outputPath, envContent, { mode: 0o600 });
124
148
  logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
125
149
  }
@@ -149,27 +149,26 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
149
149
 
150
150
  // Update REDIS_URL if present
151
151
  if (/^REDIS_URL\s*=.*$/m.test(updated)) {
152
- const m = updated.match(/^REDIS_URL\s*=\s*redis:\/\/([^:\s]+):\d+/m);
153
- const currentHost = m && m[1] ? m[1] : null;
154
- const targetHost = redisHost || currentHost;
155
- if (targetHost) {
156
- updated = updated.replace(
157
- /^REDIS_URL\s*=\s*.*$/m,
158
- `REDIS_URL=redis://${targetHost}:${redisPort}`
159
- );
160
- }
152
+ updated = updated.replace(
153
+ /^REDIS_URL\s*=\s*.*$/m,
154
+ 'REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}'
155
+ );
161
156
  }
162
157
 
163
158
  // Update REDIS_HOST if present
159
+ // If the original has host:port pattern, use ${VAR} references
160
+ // Otherwise, just set the host value
164
161
  if (/^REDIS_HOST\s*=.*$/m.test(updated)) {
165
162
  const hostPortMatch = updated.match(/^REDIS_HOST\s*=\s*([a-zA-Z0-9_.-]+):\d+$/m);
166
163
  const hasPortPattern = !!hostPortMatch;
167
164
  if (hasPortPattern) {
165
+ // Original had host:port pattern, use ${VAR} references
168
166
  updated = updated.replace(
169
167
  /^REDIS_HOST\s*=\s*.*$/m,
170
- `REDIS_HOST=${redisHost}:${redisPort}`
168
+ 'REDIS_HOST=${REDIS_HOST}:${REDIS_PORT}'
171
169
  );
172
170
  } else {
171
+ // Just host, set actual value
173
172
  updated = updated.replace(/^REDIS_HOST\s*=\s*.*$/m, `REDIS_HOST=${redisHost}`);
174
173
  }
175
174
  }
@@ -195,6 +194,31 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
195
194
  );
196
195
  }
197
196
 
197
+ // Update DATABASE_URL and other database URL variables (postgresql:// or postgres:// URLs)
198
+ // Handles DATABASE_URL, DATABASELOG_URL, and any other *_URL variables with postgresql:// or postgres://
199
+ // First, collect all matches to avoid issues with modifying string during iteration
200
+ const dbUrlPattern = /^(DATABASE[^=]*_URL)\s*=\s*(postgresql?:\/\/)([^:]+):([^@]+)@([^:/]+):(\d+)(\/[^\s]*)?/gm;
201
+ const matches = [];
202
+ let dbUrlMatch;
203
+ // Reset regex lastIndex to ensure we start from beginning
204
+ dbUrlPattern.lastIndex = 0;
205
+ while ((dbUrlMatch = dbUrlPattern.exec(updated)) !== null) {
206
+ matches.push({
207
+ varName: dbUrlMatch[1], // DATABASE_URL, DATABASELOG_URL, etc.
208
+ protocol: dbUrlMatch[2], // postgresql:// or postgres://
209
+ user: dbUrlMatch[3], // username
210
+ password: dbUrlMatch[4], // password
211
+ path: dbUrlMatch[7] || '' // /database path
212
+ });
213
+ }
214
+ // Now apply all replacements
215
+ for (const match of matches) {
216
+ updated = updated.replace(
217
+ new RegExp(`^${match.varName}\\s*=\\s*.*$`, 'm'),
218
+ `${match.varName}=${match.protocol}${match.user}:${match.password}@\${DB_HOST}:\${DB_PORT}${match.path}`
219
+ );
220
+ }
221
+
198
222
  // Update DATABASE_PORT if present (some templates use DATABASE_PORT instead of DB_PORT)
199
223
  if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
200
224
  updated = updated.replace(
@@ -203,6 +227,20 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
203
227
  );
204
228
  }
205
229
 
230
+ // Update Keycloak database variables if present
231
+ // KC_DB_URL_HOST is used by Keycloak to connect to the database
232
+ if (/^KC_DB_URL_HOST\s*=.*$/m.test(updated)) {
233
+ updated = updated.replace(/^KC_DB_URL_HOST\s*=\s*.*$/m, `KC_DB_URL_HOST=${dbHost}`);
234
+ }
235
+
236
+ // KC_DB_URL_PORT is used by Keycloak for database port
237
+ if (/^KC_DB_URL_PORT\s*=.*$/m.test(updated)) {
238
+ updated = updated.replace(
239
+ /^KC_DB_URL_PORT\s*=\s*.*$/m,
240
+ `KC_DB_URL_PORT=${dbPort}`
241
+ );
242
+ }
243
+
206
244
  return updated;
207
245
  }
208
246
 
@@ -259,6 +297,7 @@ module.exports = {
259
297
  splitHost,
260
298
  getServicePort,
261
299
  getServiceHost,
262
- updateEndpointVariables
300
+ updateEndpointVariables,
301
+ getLocalhostOverride
263
302
  };
264
303
 
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Schema Loading Utilities
3
+ *
4
+ * Loads and compiles JSON schemas for validation.
5
+ * Provides schema type detection and cached validators.
6
+ *
7
+ * @fileoverview Schema loading 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 Ajv = require('ajv');
15
+
16
+ // Cache for compiled validators
17
+ // These are reset when module is reloaded (for testing)
18
+ let externalSystemValidator = null;
19
+ let externalDataSourceValidator = null;
20
+
21
+ /**
22
+ * Reset validators cache (for testing)
23
+ * @function resetValidators
24
+ */
25
+ function resetValidators() {
26
+ externalSystemValidator = null;
27
+ externalDataSourceValidator = null;
28
+ }
29
+
30
+ /**
31
+ * Loads and compiles external-system schema
32
+ * Caches the compiled validator for performance
33
+ *
34
+ * @function loadExternalSystemSchema
35
+ * @returns {Function} Compiled AJV validator function
36
+ * @throws {Error} If schema file cannot be loaded or compiled
37
+ *
38
+ * @example
39
+ * const validate = loadExternalSystemSchema();
40
+ * const valid = validate(data);
41
+ */
42
+ function loadExternalSystemSchema() {
43
+ if (externalSystemValidator) {
44
+ return externalSystemValidator;
45
+ }
46
+
47
+ const schemaPath = path.join(__dirname, '..', 'schema', 'external-system.schema.json');
48
+
49
+ if (!fs.existsSync(schemaPath)) {
50
+ throw new Error(`External system schema not found: ${schemaPath}`);
51
+ }
52
+
53
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
54
+ let schema;
55
+
56
+ try {
57
+ schema = JSON.parse(schemaContent);
58
+ } catch (error) {
59
+ throw new Error(`Invalid JSON in external-system.schema.json: ${error.message}`);
60
+ }
61
+
62
+ const ajv = new Ajv({ allErrors: true, strict: false });
63
+ externalSystemValidator = ajv.compile(schema);
64
+
65
+ return externalSystemValidator;
66
+ }
67
+
68
+ /**
69
+ * Loads and compiles external-datasource schema
70
+ * Caches the compiled validator for performance
71
+ *
72
+ * @function loadExternalDataSourceSchema
73
+ * @returns {Function} Compiled AJV validator function
74
+ * @throws {Error} If schema file cannot be loaded or compiled
75
+ *
76
+ * @example
77
+ * const validate = loadExternalDataSourceSchema();
78
+ * const valid = validate(data);
79
+ */
80
+ function loadExternalDataSourceSchema() {
81
+ if (externalDataSourceValidator) {
82
+ return externalDataSourceValidator;
83
+ }
84
+
85
+ const schemaPath = path.join(__dirname, '..', 'schema', 'external-datasource.schema.json');
86
+
87
+ if (!fs.existsSync(schemaPath)) {
88
+ throw new Error(`External datasource schema not found: ${schemaPath}`);
89
+ }
90
+
91
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
92
+ let schema;
93
+
94
+ try {
95
+ schema = JSON.parse(schemaContent);
96
+ } catch (error) {
97
+ throw new Error(`Invalid JSON in external-datasource.schema.json: ${error.message}`);
98
+ }
99
+
100
+ // For draft-2020-12 schemas, we need to handle $schema differently
101
+ // Remove $schema if it's draft-2020-12 to avoid AJV issues
102
+ const schemaToCompile = { ...schema };
103
+ if (schemaToCompile.$schema && schemaToCompile.$schema.includes('2020-12')) {
104
+ // AJV v8 supports draft-2020-12 but may need the schema without $schema for compilation
105
+ delete schemaToCompile.$schema;
106
+ }
107
+
108
+ const ajv = new Ajv({ allErrors: true, strict: false, strictSchema: false });
109
+ externalDataSourceValidator = ajv.compile(schemaToCompile);
110
+
111
+ return externalDataSourceValidator;
112
+ }
113
+
114
+ /**
115
+ * Detects schema type from file content or path
116
+ * Attempts to identify if file is application, external-system, or external-datasource
117
+ *
118
+ * @function detectSchemaType
119
+ * @param {string} filePath - Path to the file
120
+ * @param {string} [content] - Optional file content (if not provided, will be read from file)
121
+ * @returns {string} Schema type: 'application' | 'external-system' | 'external-datasource'
122
+ * @throws {Error} If file cannot be read or parsed
123
+ *
124
+ * @example
125
+ * const type = detectSchemaType('./hubspot.json');
126
+ * // Returns: 'external-system'
127
+ */
128
+ function detectSchemaType(filePath, content) {
129
+ let fileContent = content;
130
+
131
+ // Read file if content not provided
132
+ if (!fileContent) {
133
+ if (!fs.existsSync(filePath)) {
134
+ throw new Error(`File not found: ${filePath}`);
135
+ }
136
+ fileContent = fs.readFileSync(filePath, 'utf8');
137
+ }
138
+
139
+ // Try to parse JSON
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(fileContent);
143
+ } catch (error) {
144
+ throw new Error(`Invalid JSON in file: ${error.message}`);
145
+ }
146
+
147
+ // Check for schema type indicators
148
+ // Check $id for schema type
149
+ if (parsed.$id) {
150
+ if (parsed.$id.includes('external-system')) {
151
+ return 'external-system';
152
+ }
153
+ if (parsed.$id.includes('external-datasource')) {
154
+ return 'external-datasource';
155
+ }
156
+ if (parsed.$id.includes('application-schema')) {
157
+ return 'application';
158
+ }
159
+ }
160
+
161
+ // Check title for schema type (works even without $id or $schema)
162
+ if (parsed.title) {
163
+ const titleLower = parsed.title.toLowerCase();
164
+ if (titleLower.includes('external system') || titleLower.includes('external-system') || titleLower.includes('external system configuration')) {
165
+ return 'external-system';
166
+ }
167
+ if (titleLower.includes('external data source') || titleLower.includes('external datasource') || titleLower.includes('external-datasource')) {
168
+ return 'external-datasource';
169
+ }
170
+ if (titleLower.includes('application')) {
171
+ return 'application';
172
+ }
173
+ }
174
+
175
+ // Check for required fields to determine type
176
+ // External system requires: key, displayName, description, type, authentication
177
+ if (parsed.key && parsed.displayName && parsed.type && parsed.authentication) {
178
+ // Check if it has systemKey (datasource) or not (system)
179
+ if (parsed.systemKey) {
180
+ return 'external-datasource';
181
+ }
182
+ // Check if type is one of external-system types
183
+ if (['openapi', 'mcp', 'custom'].includes(parsed.type)) {
184
+ return 'external-system';
185
+ }
186
+ }
187
+
188
+ // Check for datasource-specific fields
189
+ if (parsed.systemKey && parsed.entityKey && parsed.fieldMappings) {
190
+ return 'external-datasource';
191
+ }
192
+
193
+ // Check for application-specific fields
194
+ if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
195
+ return 'application';
196
+ }
197
+
198
+ // Fallback: check filename pattern
199
+ const fileName = path.basename(filePath).toLowerCase();
200
+ if (fileName.includes('external-system') || fileName.includes('external_system')) {
201
+ return 'external-system';
202
+ }
203
+ if (fileName.includes('external-datasource') || fileName.includes('external_datasource') || fileName.includes('datasource')) {
204
+ return 'external-datasource';
205
+ }
206
+ if (fileName.includes('application') || fileName.includes('variables') || fileName.includes('deploy')) {
207
+ return 'application';
208
+ }
209
+
210
+ // Default to application if cannot determine
211
+ return 'application';
212
+ }
213
+
214
+ module.exports = {
215
+ loadExternalSystemSchema,
216
+ loadExternalDataSourceSchema,
217
+ detectSchemaType,
218
+ resetValidators
219
+ };
220
+