@aifabrix/builder 2.41.0 → 2.42.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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +1 -1
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/jest.config.manual.js +2 -1
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +3 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/readme.js +8 -3
- package/lib/app/run-env-compose.js +64 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/show-display.js +1 -1
- package/lib/cli/setup-app.js +42 -11
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +27 -0
- package/lib/cli/setup-environment.js +12 -4
- package/lib/cli/setup-external-system.js +19 -4
- package/lib/cli/setup-infra.js +54 -14
- package/lib/cli/setup-utility.js +117 -21
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-init.js +39 -1
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/config.js +7 -1
- package/lib/core/secrets.js +33 -12
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/deployment/deployer.js +7 -5
- package/lib/external-system/download.js +182 -204
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +51 -18
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +4 -1
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +147 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/index.js +11 -3
- package/lib/infrastructure/services.js +22 -11
- package/lib/schema/application-schema.json +8 -5
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +82 -6
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +38 -10
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/compose-generator.js +1 -1
- package/lib/utils/compose-handlebars-helpers.js +11 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +115 -25
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/env-copy.js +23 -3
- package/lib/utils/error-formatters/http-status-errors.js +0 -1
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/infra-status.js +50 -44
- package/lib/utils/local-secrets.js +5 -5
- package/lib/utils/paths.js +85 -4
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +20 -0
- package/lib/utils/secrets-helpers.js +75 -89
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager.js +24 -32
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +7 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +7 -2
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +4 -4
- package/templates/typescript/docker-compose.hbs +4 -4
- package/integration/hubspot/application.yaml +0 -37
|
@@ -19,18 +19,74 @@ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
|
19
19
|
|
|
20
20
|
// Register Handlebars helper for equality check
|
|
21
21
|
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
22
|
+
handlebars.registerHelper('json', (obj) => JSON.stringify(obj));
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
+
* Build authentication object per schema authenticationVariablesByMethod.
|
|
26
|
+
* Security values use kv://<systemKey>/<key> pattern.
|
|
27
|
+
* @param {string} systemKey - External system key
|
|
28
|
+
* @param {string} authType - Auth method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none)
|
|
29
|
+
* @returns {{ method: string, variables: Object, security: Object }} Authentication object
|
|
30
|
+
*/
|
|
31
|
+
function buildAuthenticationFromMethod(systemKey, authType) {
|
|
32
|
+
const kv = (key) => `kv://${systemKey}/${key}`;
|
|
33
|
+
const method = authType || 'apikey';
|
|
34
|
+
const base = 'https://api.example.com';
|
|
35
|
+
|
|
36
|
+
const authMap = {
|
|
37
|
+
oauth2: {
|
|
38
|
+
variables: { baseUrl: base, tokenUrl: `${base}/oauth/token`, authorizationUrl: `${base}/oauth/authorize` },
|
|
39
|
+
security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
|
|
40
|
+
},
|
|
41
|
+
aad: {
|
|
42
|
+
variables: { baseUrl: base, tokenUrl: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', tenantId: '{tenant-id}' },
|
|
43
|
+
security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
|
|
44
|
+
},
|
|
45
|
+
apikey: {
|
|
46
|
+
variables: { baseUrl: base, headerName: 'X-API-Key' },
|
|
47
|
+
security: { apiKey: kv('apikey') }
|
|
48
|
+
},
|
|
49
|
+
basic: {
|
|
50
|
+
variables: { baseUrl: base },
|
|
51
|
+
security: { username: kv('username'), password: kv('password') }
|
|
52
|
+
},
|
|
53
|
+
queryParam: {
|
|
54
|
+
variables: { baseUrl: base, paramName: 'api_key' },
|
|
55
|
+
security: { paramValue: kv('paramvalue') }
|
|
56
|
+
},
|
|
57
|
+
oidc: {
|
|
58
|
+
variables: { openIdConfigUrl: 'https://example.com/.well-known/openid-configuration', clientId: 'app-id' },
|
|
59
|
+
security: {}
|
|
60
|
+
},
|
|
61
|
+
hmac: {
|
|
62
|
+
variables: { baseUrl: base, algorithm: 'sha256', signatureHeader: 'X-Signature' },
|
|
63
|
+
security: { signingSecret: kv('signingsecret') }
|
|
64
|
+
},
|
|
65
|
+
none: {
|
|
66
|
+
variables: {},
|
|
67
|
+
security: {}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const auth = authMap[method] || authMap.apikey;
|
|
72
|
+
return { method, variables: auth.variables, security: auth.security };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Target extension for format */
|
|
76
|
+
const FORMAT_EXT = { yaml: '.yaml', json: '.json' };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates external system file from template
|
|
25
80
|
* @async
|
|
26
81
|
* @function generateExternalSystemTemplate
|
|
27
82
|
* @param {string} appPath - Application directory path
|
|
28
83
|
* @param {string} systemKey - System key
|
|
29
84
|
* @param {Object} config - System configuration
|
|
85
|
+
* @param {string} [format] - Output format: 'yaml' (default) or 'json'
|
|
30
86
|
* @returns {Promise<string>} Path to generated file
|
|
31
87
|
* @throws {Error} If generation fails
|
|
32
88
|
*/
|
|
33
|
-
async function generateExternalSystemTemplate(appPath, systemKey, config) {
|
|
89
|
+
async function generateExternalSystemTemplate(appPath, systemKey, config, format = 'yaml') {
|
|
34
90
|
try {
|
|
35
91
|
const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-system.json.hbs');
|
|
36
92
|
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
@@ -41,13 +97,15 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
|
|
|
41
97
|
groups: role.groups || role.Groups || undefined
|
|
42
98
|
}));
|
|
43
99
|
|
|
100
|
+
const authType = config.authType || 'apikey';
|
|
101
|
+
const authentication = buildAuthenticationFromMethod(systemKey, authType);
|
|
102
|
+
|
|
44
103
|
const context = {
|
|
45
104
|
systemKey: systemKey,
|
|
46
105
|
systemDisplayName: config.systemDisplayName || systemKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
47
106
|
systemDescription: config.systemDescription || `External system integration for ${systemKey}`,
|
|
48
107
|
systemType: config.systemType || 'openapi',
|
|
49
|
-
|
|
50
|
-
baseUrl: config.baseUrl || null,
|
|
108
|
+
authentication,
|
|
51
109
|
roles: roles || null,
|
|
52
110
|
permissions: config.permissions || null
|
|
53
111
|
};
|
|
@@ -55,10 +113,9 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
|
|
|
55
113
|
const rendered = template(context);
|
|
56
114
|
const parsed = JSON.parse(rendered);
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
writeConfigFile(outputPath, parsed);
|
|
116
|
+
const ext = FORMAT_EXT[format === 'json' ? 'json' : 'yaml'] || '.yaml';
|
|
117
|
+
const outputPath = path.join(appPath, `${systemKey}-system${ext}`);
|
|
118
|
+
writeConfigFile(outputPath, parsed, format === 'json' ? 'json' : 'yaml');
|
|
62
119
|
|
|
63
120
|
return outputPath;
|
|
64
121
|
} catch (error) {
|
|
@@ -66,51 +123,104 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
|
|
|
66
123
|
}
|
|
67
124
|
}
|
|
68
125
|
|
|
126
|
+
/** Schema-valid entityType values (external-datasource.schema.json) */
|
|
127
|
+
const SCHEMA_ENTITY_TYPES = ['recordStorage', 'documentStorage', 'vectorStore', 'messageService', 'none'];
|
|
128
|
+
|
|
129
|
+
/** Maps resourceType to schema entityType for generation */
|
|
130
|
+
function resourceTypeToSchemaEntityType(resourceType) {
|
|
131
|
+
return resourceType === 'document' ? 'documentStorage' : 'recordStorage';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build datasource context object for template rendering
|
|
136
|
+
* @param {Object} opts - Options
|
|
137
|
+
* @param {Object} opts.config - Datasource configuration
|
|
138
|
+
* @param {string} opts.datasourceKey - Datasource key
|
|
139
|
+
* @param {Object} opts.dimensions - Dimensions map
|
|
140
|
+
* @param {Object} opts.attributes - Attributes map
|
|
141
|
+
* @param {string} opts.fullDatasourceKey - Full key including system
|
|
142
|
+
* @param {string} opts.entityKey - Entity key portion
|
|
143
|
+
* @param {string} opts.schemaEntityType - Schema entity type
|
|
144
|
+
* @param {string} opts.resourceType - Resource type
|
|
145
|
+
* @returns {Object} Handlebars context
|
|
146
|
+
*/
|
|
147
|
+
function buildDatasourceContext({ config, datasourceKey, dimensions, attributes, fullDatasourceKey, entityKey, schemaEntityType, resourceType }) {
|
|
148
|
+
const displayName = config.datasourceDisplayName || datasourceKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
149
|
+
const description = config.datasourceDescription || `External datasource for ${datasourceKey}`;
|
|
150
|
+
const primaryKey = Array.isArray(config.primaryKey) && config.primaryKey.length > 0
|
|
151
|
+
? config.primaryKey
|
|
152
|
+
: ['id'];
|
|
153
|
+
return {
|
|
154
|
+
fullDatasourceKey,
|
|
155
|
+
entityKey,
|
|
156
|
+
datasourceDisplayName: displayName,
|
|
157
|
+
datasourceDescription: description,
|
|
158
|
+
systemKey: config.systemKey,
|
|
159
|
+
schemaEntityType,
|
|
160
|
+
resourceType,
|
|
161
|
+
primaryKey,
|
|
162
|
+
systemType: config.systemType || 'openapi',
|
|
163
|
+
dimensions: Object.keys(dimensions).length > 0 ? dimensions : null,
|
|
164
|
+
attributes: Object.keys(attributes).length > 0 ? attributes : null,
|
|
165
|
+
raw: { id: '{{raw.id}}', name: '{{raw.name}}' }
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Write datasource output in requested format
|
|
171
|
+
* @param {string} outputPath - Output file path
|
|
172
|
+
* @param {string} rendered - Rendered template content
|
|
173
|
+
* @param {string} format - 'yaml' or 'json'
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
async function writeDatasourceOutput(outputPath, rendered, format) {
|
|
177
|
+
if (format === 'json') {
|
|
178
|
+
const yaml = require('js-yaml');
|
|
179
|
+
const parsed = yaml.load(rendered);
|
|
180
|
+
writeConfigFile(outputPath, parsed, 'json');
|
|
181
|
+
} else {
|
|
182
|
+
await fs.writeFile(outputPath, rendered, 'utf8');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
69
186
|
/**
|
|
70
|
-
* Generates external datasource
|
|
187
|
+
* Generates external datasource file from template with entityType-driven optional commented sections
|
|
71
188
|
* @async
|
|
72
189
|
* @function generateExternalDataSourceTemplate
|
|
73
190
|
* @param {string} appPath - Application directory path
|
|
74
|
-
* @param {string} datasourceKey - Datasource key
|
|
191
|
+
* @param {string} datasourceKey - Datasource key (e.g. entity1, company)
|
|
75
192
|
* @param {Object} config - Datasource configuration
|
|
193
|
+
* @param {string} [format] - Output format: 'yaml' (default) or 'json'
|
|
76
194
|
* @returns {Promise<string>} Path to generated file
|
|
77
195
|
* @throws {Error} If generation fails
|
|
78
196
|
*/
|
|
79
|
-
async function generateExternalDataSourceTemplate(appPath, datasourceKey, config) {
|
|
197
|
+
async function generateExternalDataSourceTemplate(appPath, datasourceKey, config, format = 'yaml') {
|
|
80
198
|
try {
|
|
81
|
-
const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-datasource.
|
|
199
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'external-datasource.yaml.hbs');
|
|
82
200
|
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
83
201
|
const template = handlebars.compile(templateContent);
|
|
84
202
|
|
|
85
203
|
const dimensions = config.dimensions || {};
|
|
86
204
|
const attributes = config.attributes || {};
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
systemKey: config.systemKey,
|
|
92
|
-
entityType: config.entityType || datasourceKey.split('-').pop(),
|
|
93
|
-
resourceType: config.resourceType || 'document',
|
|
94
|
-
systemType: config.systemType || 'openapi',
|
|
95
|
-
// Pass non-empty objects so template uses custom block; empty/null so template uses schema-valid defaults
|
|
96
|
-
dimensions: Object.keys(dimensions).length > 0 ? dimensions : null,
|
|
97
|
-
attributes: Object.keys(attributes).length > 0 ? attributes : null,
|
|
98
|
-
// Literal expression strings for default attribute block (schema: pipe-based DSL {{raw.path}})
|
|
99
|
-
raw: { id: '{{raw.id}}', name: '{{raw.name}}' }
|
|
100
|
-
};
|
|
205
|
+
const resourceType = config.resourceType || 'document';
|
|
206
|
+
const schemaEntityType = SCHEMA_ENTITY_TYPES.includes(config.entityType)
|
|
207
|
+
? config.entityType
|
|
208
|
+
: resourceTypeToSchemaEntityType(resourceType);
|
|
101
209
|
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
// Generate in same folder as application.yaml (new structure)
|
|
106
|
-
// Use naming: <app-name>-datasource-<datasource-key>.yaml
|
|
107
|
-
// Extract datasource key (remove system key prefix if present)
|
|
108
|
-
const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${config.systemKey}-`)
|
|
210
|
+
const prefix = `${config.systemKey}-`;
|
|
211
|
+
const fullDatasourceKey = datasourceKey.startsWith(prefix) ? datasourceKey : `${prefix}${datasourceKey}`;
|
|
212
|
+
const entityKey = (datasourceKey.includes('-') && datasourceKey.startsWith(prefix))
|
|
109
213
|
? datasourceKey.substring(config.systemKey.length + 1)
|
|
110
214
|
: datasourceKey;
|
|
111
|
-
const outputPath = path.join(appPath, `${config.systemKey}-datasource-${datasourceKeyOnly}.yaml`);
|
|
112
|
-
writeConfigFile(outputPath, datasourceConfig);
|
|
113
215
|
|
|
216
|
+
const context = buildDatasourceContext({
|
|
217
|
+
config, datasourceKey, dimensions, attributes, fullDatasourceKey, entityKey, schemaEntityType, resourceType
|
|
218
|
+
});
|
|
219
|
+
const rendered = template(context);
|
|
220
|
+
const ext = FORMAT_EXT[format === 'json' ? 'json' : 'yaml'] || '.yaml';
|
|
221
|
+
const outputPath = path.join(appPath, `${config.systemKey}-datasource-${entityKey}${ext}`);
|
|
222
|
+
|
|
223
|
+
await writeDatasourceOutput(outputPath, rendered, format);
|
|
114
224
|
return outputPath;
|
|
115
225
|
} catch (error) {
|
|
116
226
|
throw new Error(`Failed to generate external datasource template: ${error.message}`);
|
|
@@ -124,47 +234,51 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
|
|
|
124
234
|
* @param {string} appPath - Application directory path
|
|
125
235
|
* @param {string} appName - Application name
|
|
126
236
|
* @param {Object} config - Configuration with external system details
|
|
237
|
+
* @param {string} [format] - Output format: 'yaml' (default) or 'json'
|
|
127
238
|
* @returns {Promise<Object>} Object with system and datasource file paths
|
|
128
239
|
* @throws {Error} If generation fails
|
|
129
240
|
*/
|
|
130
|
-
async function generateExternalSystemFiles(appPath, appName, config) {
|
|
241
|
+
async function generateExternalSystemFiles(appPath, appName, config, format = 'yaml') {
|
|
131
242
|
try {
|
|
132
243
|
const systemKey = config.systemKey || appName;
|
|
133
244
|
const datasourceCount = config.datasourceCount || 1;
|
|
245
|
+
const fmt = (format === 'json' ? 'json' : 'yaml');
|
|
134
246
|
|
|
135
|
-
// Generate external system
|
|
136
|
-
const systemPath = await generateExternalSystemTemplate(appPath, systemKey, config);
|
|
247
|
+
// Generate external system file
|
|
248
|
+
const systemPath = await generateExternalSystemTemplate(appPath, systemKey, config, fmt);
|
|
137
249
|
logger.log(chalk.green(`✓ Generated external system: ${path.basename(systemPath)}`));
|
|
138
250
|
|
|
139
251
|
// Generate datasource JSON files
|
|
140
252
|
const datasourcePaths = [];
|
|
141
253
|
const resourceTypes = ['customer', 'contact', 'person', 'document', 'deal'];
|
|
142
254
|
|
|
255
|
+
const schemaEntityType = SCHEMA_ENTITY_TYPES.includes(config.entityType)
|
|
256
|
+
? config.entityType
|
|
257
|
+
: 'recordStorage';
|
|
258
|
+
|
|
143
259
|
for (let i = 0; i < datasourceCount; i++) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const datasourceKey = entityType;
|
|
260
|
+
const entityKey = `entity${i + 1}`;
|
|
261
|
+
const datasourceKey = entityKey;
|
|
147
262
|
const resourceType = resourceTypes[i % resourceTypes.length];
|
|
148
263
|
|
|
149
264
|
const datasourceConfig = {
|
|
150
265
|
systemKey: systemKey,
|
|
151
|
-
entityType:
|
|
266
|
+
entityType: schemaEntityType,
|
|
152
267
|
resourceType: resourceType,
|
|
153
268
|
systemType: config.systemType || 'openapi',
|
|
154
|
-
datasourceDisplayName: `${config.systemDisplayName || systemKey} ${
|
|
155
|
-
datasourceDescription: `External datasource for ${
|
|
269
|
+
datasourceDisplayName: `${config.systemDisplayName || systemKey} ${entityKey}`,
|
|
270
|
+
datasourceDescription: `External datasource for ${entityKey} entity`,
|
|
156
271
|
dimensions: config.dimensions || {},
|
|
157
272
|
attributes: config.attributes || {}
|
|
158
273
|
};
|
|
159
274
|
|
|
160
|
-
|
|
161
|
-
const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig);
|
|
275
|
+
const datasourcePath = await generateExternalDataSourceTemplate(appPath, datasourceKey, datasourceConfig, fmt);
|
|
162
276
|
datasourcePaths.push(datasourcePath);
|
|
163
277
|
logger.log(chalk.green(`✓ Generated datasource: ${path.basename(datasourcePath)}`));
|
|
164
278
|
}
|
|
165
279
|
|
|
166
|
-
// Update application
|
|
167
|
-
await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths);
|
|
280
|
+
// Update application config with externalIntegration block
|
|
281
|
+
await updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths, fmt);
|
|
168
282
|
|
|
169
283
|
return {
|
|
170
284
|
systemPath,
|
|
@@ -176,36 +290,70 @@ async function generateExternalSystemFiles(appPath, appName, config) {
|
|
|
176
290
|
}
|
|
177
291
|
|
|
178
292
|
/**
|
|
179
|
-
*
|
|
293
|
+
* Resolve application config path and load variables
|
|
294
|
+
* @param {string} appPath - Application directory path
|
|
295
|
+
* @param {string} ext - Config file extension
|
|
296
|
+
* @returns {{ configPath: string, variables: Object }}
|
|
297
|
+
*/
|
|
298
|
+
function resolveConfigAndVariables(appPath, ext) {
|
|
299
|
+
try {
|
|
300
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
301
|
+
const variables = loadConfigFile(configPath) || {};
|
|
302
|
+
return { configPath, variables };
|
|
303
|
+
} catch (resolveErr) {
|
|
304
|
+
const msg = (resolveErr && resolveErr.message) || '';
|
|
305
|
+
const isNotFound = msg.includes('Config file not found') || msg.includes('Application config not found');
|
|
306
|
+
if (!isNotFound) throw resolveErr;
|
|
307
|
+
return { configPath: path.join(appPath, `application${ext}`), variables: {} };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Remove old config file if it differs from target path
|
|
313
|
+
* @param {string} configPath - Current config path
|
|
314
|
+
* @param {string} targetPath - Target output path
|
|
315
|
+
*/
|
|
316
|
+
function maybeRemoveOldConfig(configPath, targetPath) {
|
|
317
|
+
const same = configPath === targetPath || path.normalize(configPath) === path.normalize(targetPath);
|
|
318
|
+
if (same) return;
|
|
319
|
+
const fsSync = require('fs');
|
|
320
|
+
if (fsSync.existsSync(configPath)) fsSync.unlinkSync(configPath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Updates application config with externalIntegration block
|
|
180
325
|
* @async
|
|
181
326
|
* @function updateVariablesYamlWithExternalIntegration
|
|
182
327
|
* @param {string} appPath - Application directory path
|
|
183
328
|
* @param {string} systemKey - System key
|
|
184
329
|
* @param {Array<string>} datasourcePaths - Array of datasource file paths
|
|
330
|
+
* @param {string} [format] - Output format: 'yaml' (default) or 'json'
|
|
185
331
|
* @throws {Error} If update fails
|
|
186
332
|
*/
|
|
187
|
-
async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths) {
|
|
333
|
+
async function updateVariablesYamlWithExternalIntegration(appPath, systemKey, datasourcePaths, format = 'yaml') {
|
|
188
334
|
try {
|
|
189
|
-
const
|
|
190
|
-
const
|
|
335
|
+
const fmt = format === 'json' ? 'json' : 'yaml';
|
|
336
|
+
const ext = FORMAT_EXT[fmt] || '.yaml';
|
|
337
|
+
const { configPath, variables } = resolveConfigAndVariables(appPath, ext);
|
|
191
338
|
|
|
192
|
-
// Add externalIntegration block
|
|
193
|
-
// Files are in same folder, so schemaBasePath is './'
|
|
194
339
|
variables.externalIntegration = {
|
|
195
340
|
schemaBasePath: './',
|
|
196
|
-
systems: [`${systemKey}-system
|
|
341
|
+
systems: [`${systemKey}-system${ext}`],
|
|
197
342
|
dataSources: datasourcePaths.map(p => path.basename(p)),
|
|
198
343
|
autopublish: true,
|
|
199
344
|
version: '1.0.0'
|
|
200
345
|
};
|
|
201
346
|
|
|
202
|
-
|
|
347
|
+
const targetPath = path.join(appPath, `application${ext}`);
|
|
348
|
+
writeConfigFile(targetPath, variables, fmt);
|
|
349
|
+
maybeRemoveOldConfig(configPath, targetPath);
|
|
203
350
|
} catch (error) {
|
|
204
351
|
throw new Error(`Failed to update application config: ${error.message}`);
|
|
205
352
|
}
|
|
206
353
|
}
|
|
207
354
|
|
|
208
355
|
module.exports = {
|
|
356
|
+
buildAuthenticationFromMethod,
|
|
209
357
|
generateExternalSystemTemplate,
|
|
210
358
|
generateExternalDataSourceTemplate,
|
|
211
359
|
generateExternalSystemFiles
|
|
@@ -32,7 +32,8 @@ async function executeDatasourceTest(systemKey, datasourceKey, payloadTemplate,
|
|
|
32
32
|
payloadTemplate,
|
|
33
33
|
dataplaneUrl,
|
|
34
34
|
authConfig,
|
|
35
|
-
timeout: parseInt(options.timeout, 10) || 30000
|
|
35
|
+
timeout: parseInt(options.timeout, 10) || 30000,
|
|
36
|
+
includeDebug: !!options.debug
|
|
36
37
|
});
|
|
37
38
|
return datasourceResult;
|
|
38
39
|
} catch (error) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-level pipeline test - single API call for all datasources
|
|
3
|
+
* @fileoverview System-level test execution for integration tests
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable max-statements,complexity -- Map response to datasource results */
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { testSystemViaPipeline } = require('../api/pipeline.api');
|
|
13
|
+
const { writeTestLog } = require('../utils/test-log-writer');
|
|
14
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run system-level pipeline test and map response to datasource results
|
|
18
|
+
* @async
|
|
19
|
+
* @param {Object} params - Parameters
|
|
20
|
+
* @param {string} params.appName - Application name
|
|
21
|
+
* @param {string} params.systemKey - System key
|
|
22
|
+
* @param {Object} params.authConfig - Auth config
|
|
23
|
+
* @param {string} params.dataplaneUrl - Dataplane URL
|
|
24
|
+
* @param {boolean} [params.debug] - Write debug log
|
|
25
|
+
* @param {number} [params.timeout] - Request timeout
|
|
26
|
+
* @returns {Promise<{success: boolean, datasourceResults: Object[]}>}
|
|
27
|
+
*/
|
|
28
|
+
async function runSystemLevelTest({ appName, systemKey, authConfig, dataplaneUrl, debug, timeout }) {
|
|
29
|
+
const testData = { includeDebug: !!debug };
|
|
30
|
+
const response = await testSystemViaPipeline(dataplaneUrl, systemKey, authConfig, testData, { timeout });
|
|
31
|
+
const data = response.data || response;
|
|
32
|
+
|
|
33
|
+
if (debug) {
|
|
34
|
+
const appPath = getIntegrationPath(appName);
|
|
35
|
+
const integrationDir = path.dirname(appPath);
|
|
36
|
+
const logPath = await writeTestLog(appName, { request: { systemKey, includeDebug: true }, response: data }, 'test-integration', integrationDir);
|
|
37
|
+
logger.log(chalk.gray(` Debug log: ${logPath}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawResults = data.datasourceResults || data.results || data.data?.datasourceResults || (Array.isArray(data) ? data : []);
|
|
41
|
+
const datasourceResults = [];
|
|
42
|
+
let success = true;
|
|
43
|
+
|
|
44
|
+
for (const r of rawResults) {
|
|
45
|
+
const dsKey = r.key || r.datasourceKey;
|
|
46
|
+
const dsResult = {
|
|
47
|
+
key: dsKey,
|
|
48
|
+
success: r.success !== false,
|
|
49
|
+
skipped: !!r.skipped,
|
|
50
|
+
reason: r.reason,
|
|
51
|
+
validationResults: r.validationResults || {},
|
|
52
|
+
fieldMappingResults: r.fieldMappingResults || {},
|
|
53
|
+
endpointTestResults: r.endpointTestResults || {}
|
|
54
|
+
};
|
|
55
|
+
if (r.error) dsResult.error = r.error;
|
|
56
|
+
if (!dsResult.success && !dsResult.skipped) success = false;
|
|
57
|
+
datasourceResults.push(dsResult);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (rawResults.length === 0 && data.success === false) {
|
|
61
|
+
success = false;
|
|
62
|
+
datasourceResults.push({
|
|
63
|
+
key: 'system',
|
|
64
|
+
success: false,
|
|
65
|
+
skipped: false,
|
|
66
|
+
error: data.error || data.formattedError || 'System test failed'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { success, datasourceResults };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { runSystemLevelTest };
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External System Testing Module
|
|
3
|
-
*
|
|
4
|
-
* Provides unit testing (local validation) and integration testing (via dataplane)
|
|
5
|
-
* for external systems and datasources.
|
|
6
|
-
*
|
|
3
|
+
* Provides unit testing (local validation) and integration testing (via dataplane).
|
|
7
4
|
* @fileoverview External system testing functionality for AI Fabrix Builder
|
|
8
5
|
* @author AI Fabrix Team
|
|
9
6
|
* @version 2.0.0
|
|
10
7
|
*/
|
|
8
|
+
/* eslint-disable max-lines,max-lines-per-function,max-statements,complexity,max-depth -- Integration test flow with system-level and per-datasource paths */
|
|
11
9
|
|
|
12
10
|
const fs = require('fs').promises;
|
|
13
11
|
const fsSync = require('fs');
|
|
@@ -36,6 +34,7 @@ const {
|
|
|
36
34
|
const {
|
|
37
35
|
testSingleDatasourceIntegration
|
|
38
36
|
} = require('./test-execution');
|
|
37
|
+
const { runSystemLevelTest } = require('./test-system-level');
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Loads and parses application config file
|
|
@@ -426,22 +425,56 @@ async function testExternalSystemIntegration(appName, options = {}) {
|
|
|
426
425
|
datasourceResults: []
|
|
427
426
|
};
|
|
428
427
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
const useSystemLevel = !options.datasource && datasourcesToTest.length > 0 && !customPayload;
|
|
429
|
+
const timeout = parseInt(options.timeout, 10) || 30000;
|
|
430
|
+
|
|
431
|
+
if (useSystemLevel && datasourcesToTest.length > 1) {
|
|
432
|
+
try {
|
|
433
|
+
const systemResult = await runSystemLevelTest({
|
|
434
|
+
appName, systemKey, authConfig, dataplaneUrl, debug: options.debug, timeout
|
|
435
|
+
});
|
|
436
|
+
results.success = systemResult.success;
|
|
437
|
+
results.datasourceResults = systemResult.datasourceResults;
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (options.debug) {
|
|
440
|
+
try {
|
|
441
|
+
const { writeTestLog } = require('../utils/test-log-writer');
|
|
442
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
443
|
+
const appPath = getIntegrationPath(appName);
|
|
444
|
+
await writeTestLog(appName, { request: { systemKey }, error: err.message }, 'test-integration', path.dirname(appPath));
|
|
445
|
+
} catch (_) { /* ignore */ }
|
|
446
|
+
}
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
for (const datasourceFile of datasourcesToTest) {
|
|
451
|
+
const datasourceResult = await testSingleDatasourceIntegration(
|
|
452
|
+
datasourceFile,
|
|
453
|
+
systemKey,
|
|
454
|
+
dataplaneUrl,
|
|
455
|
+
authConfig,
|
|
456
|
+
customPayload,
|
|
457
|
+
options
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
if (!datasourceResult.success && !datasourceResult.skipped) {
|
|
461
|
+
results.success = false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
results.datasourceResults.push(datasourceResult);
|
|
442
465
|
}
|
|
443
466
|
|
|
444
|
-
results.datasourceResults.
|
|
467
|
+
if (options.debug && results.datasourceResults.length > 0) {
|
|
468
|
+
try {
|
|
469
|
+
const { writeTestLog } = require('../utils/test-log-writer');
|
|
470
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
471
|
+
const appPath = getIntegrationPath(appName);
|
|
472
|
+
await writeTestLog(appName, {
|
|
473
|
+
request: { systemKey, datasource: options.datasource, includeDebug: true },
|
|
474
|
+
response: results
|
|
475
|
+
}, 'test-integration', path.dirname(appPath));
|
|
476
|
+
} catch (_) { /* ignore */ }
|
|
477
|
+
}
|
|
445
478
|
}
|
|
446
479
|
|
|
447
480
|
return results;
|
|
@@ -92,6 +92,7 @@ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
|
|
|
92
92
|
* @param {string} appName - Application name
|
|
93
93
|
* @param {Object} [options] - Optional parameters
|
|
94
94
|
* @param {string} [options.appPath] - Application path (if provided, skips detection)
|
|
95
|
+
* @param {boolean} [options.skipMissingDatasourceFiles] - If true, skip datasource files that don't exist (e.g. when generating deploy JSON)
|
|
95
96
|
* @returns {Promise<Object>} Controller manifest object
|
|
96
97
|
* @throws {Error} If generation fails
|
|
97
98
|
*
|
|
@@ -125,7 +126,9 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
125
126
|
}
|
|
126
127
|
const [systemJson, datasourceJsons] = await Promise.all([
|
|
127
128
|
loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]),
|
|
128
|
-
loadDatasourceFiles(appPath, schemaBasePath, variables.externalIntegration.dataSources || []
|
|
129
|
+
loadDatasourceFiles(appPath, schemaBasePath, variables.externalIntegration.dataSources || [], {
|
|
130
|
+
skipMissingDatasourceFiles: options.skipMissingDatasourceFiles
|
|
131
|
+
})
|
|
129
132
|
]);
|
|
130
133
|
const appVersion = variables.app?.version || variables.externalIntegration?.version || '1.0.0';
|
|
131
134
|
const externalIntegration = {
|
|
@@ -150,6 +153,30 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
150
153
|
};
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Returns deploy-file shape: system + dataSources only (no file-name lists).
|
|
158
|
+
* Use when writing *-deploy.json to disk; file names live in application config only.
|
|
159
|
+
*
|
|
160
|
+
* @function toDeployJsonShape
|
|
161
|
+
* @param {Object} manifest - Full controller manifest from generateControllerManifest
|
|
162
|
+
* @returns {Object} { key, displayName, description, type, version, system, dataSources }
|
|
163
|
+
*/
|
|
164
|
+
function toDeployJsonShape(manifest) {
|
|
165
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
166
|
+
throw new Error('Manifest is required');
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
key: manifest.key,
|
|
170
|
+
displayName: manifest.displayName,
|
|
171
|
+
description: manifest.description,
|
|
172
|
+
type: manifest.type || 'external',
|
|
173
|
+
version: manifest.version || '1.0.0',
|
|
174
|
+
system: manifest.system,
|
|
175
|
+
dataSources: Array.isArray(manifest.dataSources) ? manifest.dataSources : []
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
module.exports = {
|
|
154
|
-
generateControllerManifest
|
|
180
|
+
generateControllerManifest,
|
|
181
|
+
toDeployJsonShape
|
|
155
182
|
};
|
|
@@ -192,7 +192,7 @@ async function writeSplitExternalSchemaFiles({ outputDir, systemKey, application
|
|
|
192
192
|
const rbac = extractRbacYaml(application);
|
|
193
193
|
let rbacPath = null;
|
|
194
194
|
if (rbac) {
|
|
195
|
-
rbacPath = path.join(outputDir, 'rbac.
|
|
195
|
+
rbacPath = path.join(outputDir, 'rbac.yaml');
|
|
196
196
|
await writeYamlFile(rbacPath, rbac, { indent: 2, lineWidth: -1 });
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -174,11 +174,14 @@ async function loadSystemFile(appPath, schemaBasePath, systemFileName) {
|
|
|
174
174
|
* @param {string} appPath - Application path
|
|
175
175
|
* @param {string} schemaBasePath - Schema base path
|
|
176
176
|
* @param {Array<string>} datasourceFiles - Array of datasource file names
|
|
177
|
+
* @param {Object} [options] - Options
|
|
178
|
+
* @param {boolean} [options.skipMissingDatasourceFiles] - If true, skip missing files (e.g. when generating deploy JSON) instead of throwing
|
|
177
179
|
* @returns {Promise<Array<Object>>} Array of datasource JSON objects
|
|
178
|
-
* @throws {Error} If
|
|
180
|
+
* @throws {Error} If a file is missing and skipMissingDatasourceFiles is not set
|
|
179
181
|
*/
|
|
180
|
-
async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles) {
|
|
182
|
+
async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles, options = {}) {
|
|
181
183
|
const datasourceJsons = [];
|
|
184
|
+
const { skipMissingDatasourceFiles } = options;
|
|
182
185
|
|
|
183
186
|
for (const datasourceFile of datasourceFiles) {
|
|
184
187
|
const datasourcePath = path.isAbsolute(schemaBasePath)
|
|
@@ -186,6 +189,9 @@ async function loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles) {
|
|
|
186
189
|
: path.join(appPath, schemaBasePath, datasourceFile);
|
|
187
190
|
|
|
188
191
|
if (!fs.existsSync(datasourcePath)) {
|
|
192
|
+
if (skipMissingDatasourceFiles) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
189
195
|
throw new Error(`Datasource file not found: ${datasourcePath}`);
|
|
190
196
|
}
|
|
191
197
|
|
|
@@ -417,6 +423,7 @@ module.exports = {
|
|
|
417
423
|
generateExternalSystemApplicationSchema,
|
|
418
424
|
splitExternalApplicationSchema,
|
|
419
425
|
loadSystemFile,
|
|
420
|
-
loadDatasourceFiles
|
|
426
|
+
loadDatasourceFiles,
|
|
427
|
+
loadExternalIntegrationConfig
|
|
421
428
|
};
|
|
422
429
|
|
package/lib/generator/index.js
CHANGED
|
@@ -249,7 +249,10 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
249
249
|
* @returns {Promise<string>} Path to written deploy JSON
|
|
250
250
|
*/
|
|
251
251
|
async function writeExternalDeployJson(appName, appPath, options) {
|
|
252
|
-
const manifest = await generateControllerManifest(appName,
|
|
252
|
+
const manifest = await generateControllerManifest(appName, {
|
|
253
|
+
...options,
|
|
254
|
+
skipMissingDatasourceFiles: true
|
|
255
|
+
});
|
|
253
256
|
let effectivePort = 3000;
|
|
254
257
|
try {
|
|
255
258
|
const variablesPath = resolveApplicationConfigPath(appPath);
|