@aifabrix/builder 2.8.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 (36) hide show
  1. package/integration/hubspot/README.md +136 -0
  2. package/integration/hubspot/env.template +9 -0
  3. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  4. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  5. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  6. package/integration/hubspot/hubspot-deploy.json +91 -0
  7. package/integration/hubspot/variables.yaml +17 -0
  8. package/lib/app-config.js +4 -3
  9. package/lib/app-deploy.js +8 -20
  10. package/lib/app-dockerfile.js +7 -9
  11. package/lib/app-prompts.js +6 -5
  12. package/lib/app-push.js +9 -9
  13. package/lib/app-register.js +23 -5
  14. package/lib/app-rotate-secret.js +10 -0
  15. package/lib/app-run.js +5 -11
  16. package/lib/app.js +42 -14
  17. package/lib/build.js +20 -16
  18. package/lib/cli.js +61 -2
  19. package/lib/datasource-deploy.js +14 -20
  20. package/lib/external-system-deploy.js +123 -40
  21. package/lib/external-system-download.js +431 -0
  22. package/lib/external-system-generator.js +13 -10
  23. package/lib/external-system-test.js +446 -0
  24. package/lib/generator-builders.js +323 -0
  25. package/lib/generator.js +200 -292
  26. package/lib/schema/application-schema.json +853 -852
  27. package/lib/schema/external-datasource.schema.json +823 -49
  28. package/lib/schema/external-system.schema.json +96 -78
  29. package/lib/templates.js +1 -1
  30. package/lib/utils/cli-utils.js +4 -4
  31. package/lib/utils/external-system-display.js +159 -0
  32. package/lib/utils/external-system-validators.js +245 -0
  33. package/lib/utils/paths.js +151 -1
  34. package/lib/utils/schema-resolver.js +7 -2
  35. package/lib/validator.js +5 -2
  36. package/package.json +1 -1
@@ -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}`);
package/lib/validator.js CHANGED
@@ -19,6 +19,7 @@ const externalDataSourceSchema = require('./schema/external-datasource.schema.js
19
19
  const { transformVariablesForValidation } = require('./utils/variable-transformer');
20
20
  const { checkEnvironment } = require('./utils/environment-checker');
21
21
  const { formatValidationErrors } = require('./utils/error-formatter');
22
+ const { detectAppType } = require('./utils/paths');
22
23
 
23
24
  /**
24
25
  * Validates variables.yaml file against application schema
@@ -39,7 +40,9 @@ async function validateVariables(appName) {
39
40
  throw new Error('App name is required and must be a string');
40
41
  }
41
42
 
42
- 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');
43
46
 
44
47
  if (!fs.existsSync(variablesPath)) {
45
48
  throw new Error(`variables.yaml not found: ${variablesPath}`);
@@ -254,7 +257,7 @@ async function validateEnvTemplate(appName) {
254
257
 
255
258
  /**
256
259
  * Validates deployment JSON against application schema
257
- * Ensures generated aifabrix-deploy.json matches the schema structure
260
+ * Ensures generated <app-name>-deploy.json matches the schema structure
258
261
  *
259
262
  * @function validateDeploymentJson
260
263
  * @param {Object} deployment - Deployment JSON object to validate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {