@friggframework/devtools 2.0.0--canary.461.849e166.0 → 2.0.0--canary.474.aa465e4.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/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/domains/database/aurora-builder.js +234 -57
- package/infrastructure/domains/database/aurora-builder.test.js +7 -2
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +256 -215
- package/infrastructure/domains/database/migration-builder.test.js +5 -111
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/integration/integration-builder.js +258 -84
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/networking/vpc-builder.js +856 -135
- package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/security/kms-builder.js +179 -22
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/package.json +6 -6
- package/infrastructure/REFACTOR.md +0 -532
- package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration Builder
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Domain Layer - Hexagonal Architecture
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Responsible for:
|
|
7
|
-
* - Creating SQS queues for each integration
|
|
7
|
+
* - Creating SQS queues for each integration (CloudFormation resources)
|
|
8
|
+
* - Creating InternalErrorQueue (dead letter queue)
|
|
9
|
+
* - Creating Lambda function definitions (serverless template code)
|
|
8
10
|
* - Creating queue worker Lambda functions
|
|
9
11
|
* - Creating webhook handler functions
|
|
10
12
|
* - Configuring integration-specific routes and handlers
|
|
13
|
+
*
|
|
14
|
+
* Uses ownership-based architecture to support both stack-managed
|
|
15
|
+
* and externally-provided SQS queues.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
|
|
19
|
+
const IntegrationResourceResolver = require('./integration-resolver');
|
|
20
|
+
const { createEmptyDiscoveryResult, ResourceOwnership } = require('../shared/types');
|
|
14
21
|
|
|
15
22
|
class IntegrationBuilder extends InfrastructureBuilder {
|
|
16
23
|
constructor() {
|
|
@@ -22,6 +29,10 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
22
29
|
return Array.isArray(appDefinition.integrations) && appDefinition.integrations.length > 0;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
getDependencies() {
|
|
33
|
+
return []; // No dependencies - integrations can run independently
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
validate(appDefinition) {
|
|
26
37
|
const result = new ValidationResult();
|
|
27
38
|
|
|
@@ -45,7 +56,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/**
|
|
48
|
-
* Build integration infrastructure
|
|
59
|
+
* Build integration infrastructure using ownership-based architecture
|
|
49
60
|
*/
|
|
50
61
|
async build(appDefinition, discoveredResources) {
|
|
51
62
|
console.log(`\n[${this.name}] Configuring integrations...`);
|
|
@@ -56,9 +67,123 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
56
67
|
resources: {},
|
|
57
68
|
environment: {},
|
|
58
69
|
custom: {},
|
|
70
|
+
iamStatements: [],
|
|
59
71
|
};
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
// Get structured discovery result
|
|
74
|
+
const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources);
|
|
75
|
+
|
|
76
|
+
// Use IntegrationResourceResolver to make ownership decisions
|
|
77
|
+
const resolver = new IntegrationResourceResolver();
|
|
78
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
79
|
+
|
|
80
|
+
console.log('\n 📋 Resource Ownership Decisions:');
|
|
81
|
+
console.log(` InternalErrorQueue: ${decisions.internalErrorQueue.ownership} - ${decisions.internalErrorQueue.reason}`);
|
|
82
|
+
|
|
83
|
+
// Log per-integration decisions
|
|
84
|
+
Object.keys(decisions.integrations).forEach(integrationName => {
|
|
85
|
+
const queueDecision = decisions.integrations[integrationName].queue;
|
|
86
|
+
console.log(` ${integrationName}Queue: ${queueDecision.ownership} - ${queueDecision.reason}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Build resources based on ownership decisions
|
|
90
|
+
await this.buildFromDecisions(decisions, appDefinition, result);
|
|
91
|
+
|
|
92
|
+
console.log(`[${this.name}] ✅ Integration configuration completed`);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert flat discovery to structured discovery
|
|
98
|
+
* Provides backwards compatibility
|
|
99
|
+
*/
|
|
100
|
+
convertFlatDiscoveryToStructured(flatDiscovery) {
|
|
101
|
+
const discovery = createEmptyDiscoveryResult();
|
|
102
|
+
|
|
103
|
+
if (!flatDiscovery) {
|
|
104
|
+
return discovery;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if resources are from CloudFormation stack
|
|
108
|
+
if (flatDiscovery.fromCloudFormationStack) {
|
|
109
|
+
discovery.fromCloudFormation = true;
|
|
110
|
+
discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
|
|
111
|
+
|
|
112
|
+
// Add stack-managed resources from existingLogicalIds
|
|
113
|
+
const existingLogicalIds = flatDiscovery.existingLogicalIds || [];
|
|
114
|
+
existingLogicalIds.forEach(logicalId => {
|
|
115
|
+
let resourceType = '';
|
|
116
|
+
let physicalId = '';
|
|
117
|
+
|
|
118
|
+
// Determine resource type and physical ID
|
|
119
|
+
if (logicalId === 'InternalErrorQueue') {
|
|
120
|
+
resourceType = 'AWS::SQS::Queue';
|
|
121
|
+
physicalId = flatDiscovery.internalErrorQueueUrl;
|
|
122
|
+
} else if (logicalId.endsWith('Queue')) {
|
|
123
|
+
// Integration-specific queue (e.g., SlackQueue, HubspotQueue)
|
|
124
|
+
resourceType = 'AWS::SQS::Queue';
|
|
125
|
+
const integrationName = logicalId.replace('Queue', '').toLowerCase();
|
|
126
|
+
physicalId = flatDiscovery[`${integrationName}QueueUrl`];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (physicalId && typeof physicalId === 'string') {
|
|
130
|
+
discovery.stackManaged.push({
|
|
131
|
+
logicalId,
|
|
132
|
+
physicalId,
|
|
133
|
+
resourceType
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return discovery;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build integration resources based on ownership decisions
|
|
144
|
+
*/
|
|
145
|
+
async buildFromDecisions(decisions, appDefinition, result) {
|
|
146
|
+
// Create InternalErrorQueue if ownership = STACK
|
|
147
|
+
const shouldCreateInternalErrorQueue = decisions.internalErrorQueue.ownership === ResourceOwnership.STACK;
|
|
148
|
+
|
|
149
|
+
if (shouldCreateInternalErrorQueue) {
|
|
150
|
+
console.log(' → Creating InternalErrorQueue in stack');
|
|
151
|
+
this.createInternalErrorQueue(result);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(' → Using external InternalErrorQueue');
|
|
154
|
+
this.useExternalInternalErrorQueue(decisions.internalErrorQueue, result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Create Lambda function definitions and queue resources for each integration
|
|
158
|
+
const functionPackageConfig = this.createFunctionPackageConfig();
|
|
159
|
+
|
|
160
|
+
for (const integration of appDefinition.integrations) {
|
|
161
|
+
const integrationName = integration.Definition.name;
|
|
162
|
+
const queueDecision = decisions.integrations[integrationName].queue;
|
|
163
|
+
|
|
164
|
+
console.log(`\n Adding integration: ${integrationName}`);
|
|
165
|
+
|
|
166
|
+
// Create Lambda function definitions (serverless template code)
|
|
167
|
+
await this.createFunctionDefinitions(integration, functionPackageConfig, result);
|
|
168
|
+
|
|
169
|
+
// Create or reference SQS queue based on ownership decision
|
|
170
|
+
const shouldCreateQueue = queueDecision.ownership === ResourceOwnership.STACK;
|
|
171
|
+
|
|
172
|
+
if (shouldCreateQueue) {
|
|
173
|
+
console.log(` ✓ Creating ${integrationName}Queue in stack`);
|
|
174
|
+
this.createIntegrationQueue(integrationName, result);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` ✓ Using external ${integrationName}Queue`);
|
|
177
|
+
this.useExternalIntegrationQueue(integrationName, queueDecision, result);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create function package exclusion configuration
|
|
184
|
+
*/
|
|
185
|
+
createFunctionPackageConfig() {
|
|
186
|
+
return {
|
|
62
187
|
exclude: [
|
|
63
188
|
// Exclude AWS SDK (provided by Lambda runtime)
|
|
64
189
|
'node_modules/aws-sdk/**',
|
|
@@ -118,106 +243,155 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
118
243
|
'**/.swarm/**',
|
|
119
244
|
],
|
|
120
245
|
};
|
|
246
|
+
}
|
|
121
247
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
result.functions[webhookFunctionName] = {
|
|
137
|
-
handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
|
|
138
|
-
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
139
|
-
package: functionPackageConfig,
|
|
140
|
-
events: [
|
|
141
|
-
{
|
|
142
|
-
httpApi: {
|
|
143
|
-
path: `/api/${integrationName}-integration/webhooks`,
|
|
144
|
-
method: 'POST',
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
httpApi: {
|
|
149
|
-
path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
|
|
150
|
-
method: 'POST',
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
};
|
|
155
|
-
console.log(` + Webhook handler enabled`);
|
|
156
|
-
}
|
|
248
|
+
/**
|
|
249
|
+
* Create Lambda function definitions for an integration
|
|
250
|
+
* These are serverless framework template function definitions
|
|
251
|
+
*/
|
|
252
|
+
async createFunctionDefinitions(integration, functionPackageConfig, result) {
|
|
253
|
+
const integrationName = integration.Definition.name;
|
|
254
|
+
|
|
255
|
+
// Add webhook handler if enabled (BEFORE catch-all proxy route)
|
|
256
|
+
// CRITICAL: Webhook routes must be defined before the catch-all {proxy+} route
|
|
257
|
+
// to ensure proper route matching in AWS API Gateway/HTTP API
|
|
258
|
+
const webhookConfig = integration.Definition.webhooks;
|
|
259
|
+
if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
|
|
260
|
+
const webhookFunctionName = `${integrationName}Webhook`;
|
|
157
261
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
262
|
+
result.functions[webhookFunctionName] = {
|
|
263
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
|
|
161
264
|
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
162
265
|
package: functionPackageConfig,
|
|
163
266
|
events: [
|
|
164
267
|
{
|
|
165
268
|
httpApi: {
|
|
166
|
-
path: `/api/${integrationName}-integration/
|
|
167
|
-
method: '
|
|
269
|
+
path: `/api/${integrationName}-integration/webhooks`,
|
|
270
|
+
method: 'POST',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
httpApi: {
|
|
275
|
+
path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
|
|
276
|
+
method: 'POST',
|
|
168
277
|
},
|
|
169
278
|
},
|
|
170
279
|
],
|
|
171
280
|
};
|
|
281
|
+
console.log(` ✓ Webhook handler function defined`);
|
|
282
|
+
}
|
|
172
283
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
184
|
-
},
|
|
284
|
+
// Create HTTP API handler for integration (catch-all route AFTER webhooks)
|
|
285
|
+
result.functions[integrationName] = {
|
|
286
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
287
|
+
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
288
|
+
package: functionPackageConfig,
|
|
289
|
+
events: [
|
|
290
|
+
{
|
|
291
|
+
httpApi: {
|
|
292
|
+
path: `/api/${integrationName}-integration/{proxy+}`,
|
|
293
|
+
method: 'ANY',
|
|
185
294
|
},
|
|
186
295
|
},
|
|
187
|
-
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
console.log(` ✓ HTTP handler function defined`);
|
|
188
299
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
300
|
+
// Create Queue Worker function
|
|
301
|
+
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
302
|
+
result.functions[queueWorkerName] = {
|
|
303
|
+
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
304
|
+
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
305
|
+
package: functionPackageConfig,
|
|
306
|
+
reservedConcurrency: 5,
|
|
307
|
+
events: [
|
|
308
|
+
{
|
|
309
|
+
sqs: {
|
|
310
|
+
arn: { 'Fn::GetAtt': [`${this.capitalizeFirst(integrationName)}Queue`, 'Arn'] },
|
|
311
|
+
batchSize: 1,
|
|
202
312
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
timeout: 900, // 15 minutes max for queue workers (Lambda maximum)
|
|
316
|
+
};
|
|
317
|
+
console.log(` ✓ Queue worker function defined`);
|
|
318
|
+
}
|
|
206
319
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Create InternalErrorQueue CloudFormation resource
|
|
322
|
+
*/
|
|
323
|
+
createInternalErrorQueue(result) {
|
|
324
|
+
result.resources.InternalErrorQueue = {
|
|
325
|
+
Type: 'AWS::SQS::Queue',
|
|
326
|
+
Properties: {
|
|
327
|
+
QueueName: '${self:service}-${self:provider.stage}-InternalErrorQueue',
|
|
328
|
+
MessageRetentionPeriod: 1209600, // 14 days
|
|
329
|
+
VisibilityTimeout: 300, // 5 minutes for error processing
|
|
330
|
+
},
|
|
331
|
+
};
|
|
211
332
|
|
|
212
|
-
|
|
213
|
-
|
|
333
|
+
console.log(' ✓ Created InternalErrorQueue resource');
|
|
334
|
+
}
|
|
214
335
|
|
|
215
|
-
|
|
216
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Use external InternalErrorQueue
|
|
338
|
+
*/
|
|
339
|
+
useExternalInternalErrorQueue(decision, result) {
|
|
340
|
+
// Add ARN to environment for Lambda functions
|
|
341
|
+
result.environment.INTERNAL_ERROR_QUEUE_ARN = decision.physicalId;
|
|
217
342
|
|
|
218
|
-
|
|
343
|
+
console.log(` ✓ Using external InternalErrorQueue: ${decision.physicalId}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create integration-specific SQS queue CloudFormation resource
|
|
348
|
+
*/
|
|
349
|
+
createIntegrationQueue(integrationName, result) {
|
|
350
|
+
const queueReference = `${this.capitalizeFirst(integrationName)}Queue`;
|
|
351
|
+
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
352
|
+
|
|
353
|
+
result.resources[queueReference] = {
|
|
354
|
+
Type: 'AWS::SQS::Queue',
|
|
355
|
+
Properties: {
|
|
356
|
+
QueueName: `\${self:custom.${queueReference}}`,
|
|
357
|
+
MessageRetentionPeriod: 60,
|
|
358
|
+
VisibilityTimeout: 1800,
|
|
359
|
+
RedrivePolicy: {
|
|
360
|
+
maxReceiveCount: 1,
|
|
361
|
+
deadLetterTargetArn: {
|
|
362
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Add queue URL to environment
|
|
369
|
+
result.environment[`${integrationName.toUpperCase()}_QUEUE_URL`] = {
|
|
370
|
+
Ref: queueReference,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Add queue name to custom section
|
|
374
|
+
result.custom[queueReference] = queueName;
|
|
375
|
+
|
|
376
|
+
console.log(` ✓ Created ${queueReference} resource`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Use external integration queue
|
|
381
|
+
*/
|
|
382
|
+
useExternalIntegrationQueue(integrationName, decision, result) {
|
|
383
|
+
// Add queue URL to environment for Lambda functions
|
|
384
|
+
result.environment[`${integrationName.toUpperCase()}_QUEUE_URL`] = decision.physicalId;
|
|
385
|
+
|
|
386
|
+
console.log(` ✓ Using external queue: ${decision.physicalId}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Capitalize first letter of string (e.g., 'slack' -> 'Slack')
|
|
391
|
+
*/
|
|
392
|
+
capitalizeFirst(str) {
|
|
393
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
219
394
|
}
|
|
220
395
|
}
|
|
221
396
|
|
|
222
397
|
module.exports = { IntegrationBuilder };
|
|
223
|
-
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Resource Resolver
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Responsible for resolving ownership decisions for integration infrastructure:
|
|
7
|
+
* - SQS queues per integration
|
|
8
|
+
*
|
|
9
|
+
* Follows the ownership-based architecture pattern:
|
|
10
|
+
* - STACK: Create/manage resources in CloudFormation stack
|
|
11
|
+
* - EXTERNAL: Use existing resources by physical ID
|
|
12
|
+
* - AUTO: Intelligent decision based on discovery
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BaseResourceResolver = require('../shared/base-resolver');
|
|
16
|
+
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
17
|
+
|
|
18
|
+
class IntegrationResourceResolver extends BaseResourceResolver {
|
|
19
|
+
/**
|
|
20
|
+
* Resolve ownership for all integration resources
|
|
21
|
+
* @param {Object} appDefinition - Application definition
|
|
22
|
+
* @param {Object} discovery - Structured discovery result
|
|
23
|
+
* @returns {Object} Ownership decisions for all integration resources
|
|
24
|
+
*/
|
|
25
|
+
resolveAll(appDefinition, discovery) {
|
|
26
|
+
const integrations = appDefinition.integrations || [];
|
|
27
|
+
const decisions = {
|
|
28
|
+
// Shared resources used by all integrations
|
|
29
|
+
internalErrorQueue: this.resolveInternalErrorQueue(appDefinition, discovery),
|
|
30
|
+
// Per-integration queues
|
|
31
|
+
integrations: {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Resolve ownership for each integration's SQS queue
|
|
35
|
+
integrations.forEach(integration => {
|
|
36
|
+
const integrationName = integration.Definition?.name;
|
|
37
|
+
if (!integrationName) {
|
|
38
|
+
return; // Skip invalid integrations
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
decisions.integrations[integrationName] = {
|
|
42
|
+
queue: this.resolveQueue(integrationName, appDefinition, discovery),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return decisions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve ownership for the shared InternalErrorQueue (dead letter queue)
|
|
51
|
+
* @param {Object} appDefinition - Application definition
|
|
52
|
+
* @param {Object} discovery - Structured discovery result
|
|
53
|
+
* @returns {Object} Ownership decision for InternalErrorQueue
|
|
54
|
+
*/
|
|
55
|
+
resolveInternalErrorQueue(appDefinition, discovery) {
|
|
56
|
+
const userIntent = appDefinition.integrations?.ownership?.internalErrorQueue || ResourceOwnership.AUTO;
|
|
57
|
+
const queueLogicalId = 'InternalErrorQueue';
|
|
58
|
+
const inStack = this.isInStack(queueLogicalId, discovery);
|
|
59
|
+
|
|
60
|
+
// STACK: User wants this in the CloudFormation stack
|
|
61
|
+
if (userIntent === ResourceOwnership.STACK) {
|
|
62
|
+
const stackResource = inStack ? this.findInStack(queueLogicalId, discovery) : null;
|
|
63
|
+
return this.createStackDecision(
|
|
64
|
+
stackResource?.physicalId || null,
|
|
65
|
+
inStack
|
|
66
|
+
? `Found ${queueLogicalId} in CloudFormation stack`
|
|
67
|
+
: `Will create ${queueLogicalId} in stack`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// EXTERNAL: User wants to use an existing queue
|
|
72
|
+
if (userIntent === ResourceOwnership.EXTERNAL) {
|
|
73
|
+
const queueArn = appDefinition.integrations?.internalErrorQueue?.arn;
|
|
74
|
+
if (!queueArn) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'InternalErrorQueue configured with ownership=external but integrations.internalErrorQueue.arn not provided'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return this.createExternalDecision(
|
|
80
|
+
queueArn,
|
|
81
|
+
`Using external InternalErrorQueue ARN: ${queueArn}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// AUTO: Intelligent decision based on discovery
|
|
86
|
+
if (inStack) {
|
|
87
|
+
const stackResource = this.findInStack(queueLogicalId, discovery);
|
|
88
|
+
return this.createStackDecision(
|
|
89
|
+
stackResource.physicalId,
|
|
90
|
+
`Found ${queueLogicalId} in CloudFormation stack - will include definition (idempotent)`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Default: Create in stack
|
|
95
|
+
return this.createStackDecision(
|
|
96
|
+
null,
|
|
97
|
+
`No existing ${queueLogicalId} found - will create in stack`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve ownership for an integration's SQS queue
|
|
103
|
+
* @param {string} integrationName - Name of the integration (e.g., 'slack')
|
|
104
|
+
* @param {Object} appDefinition - Application definition
|
|
105
|
+
* @param {Object} discovery - Structured discovery result
|
|
106
|
+
* @returns {Object} Ownership decision for the queue
|
|
107
|
+
*/
|
|
108
|
+
resolveQueue(integrationName, appDefinition, discovery) {
|
|
109
|
+
// Check for explicit ownership configuration
|
|
110
|
+
const integration = appDefinition.integrations?.find(
|
|
111
|
+
i => i.Definition?.name === integrationName
|
|
112
|
+
);
|
|
113
|
+
const userIntent = integration?.ownership?.queue || ResourceOwnership.AUTO;
|
|
114
|
+
|
|
115
|
+
// Queue logical ID follows pattern: ${IntegrationName}Queue (e.g., SlackQueue)
|
|
116
|
+
const queueLogicalId = `${this.capitalizeFirst(integrationName)}Queue`;
|
|
117
|
+
const inStack = this.isInStack(queueLogicalId, discovery);
|
|
118
|
+
|
|
119
|
+
// STACK: User wants this in the CloudFormation stack
|
|
120
|
+
if (userIntent === ResourceOwnership.STACK) {
|
|
121
|
+
const stackResource = inStack ? this.findInStack(queueLogicalId, discovery) : null;
|
|
122
|
+
return this.createStackDecision(
|
|
123
|
+
stackResource?.physicalId || null,
|
|
124
|
+
inStack
|
|
125
|
+
? `Found ${queueLogicalId} in CloudFormation stack`
|
|
126
|
+
: `Will create ${queueLogicalId} in stack`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// EXTERNAL: User wants to use an existing queue
|
|
131
|
+
if (userIntent === ResourceOwnership.EXTERNAL) {
|
|
132
|
+
const queueUrl = integration?.queue?.url;
|
|
133
|
+
if (!queueUrl) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Integration '${integrationName}' configured with ownership=external but queue.url not provided`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return this.createExternalDecision(
|
|
139
|
+
queueUrl,
|
|
140
|
+
`Using external queue URL: ${queueUrl}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// AUTO: Intelligent decision based on discovery
|
|
145
|
+
if (inStack) {
|
|
146
|
+
const stackResource = this.findInStack(queueLogicalId, discovery);
|
|
147
|
+
return this.createStackDecision(
|
|
148
|
+
stackResource.physicalId,
|
|
149
|
+
`Found ${queueLogicalId} in CloudFormation stack - will include definition (idempotent)`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Default: Create in stack
|
|
154
|
+
return this.createStackDecision(
|
|
155
|
+
null,
|
|
156
|
+
`No existing ${queueLogicalId} found - will create in stack`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Capitalize first letter of string (e.g., 'slack' -> 'Slack')
|
|
162
|
+
* @param {string} str - String to capitalize
|
|
163
|
+
* @returns {string} Capitalized string
|
|
164
|
+
*/
|
|
165
|
+
capitalizeFirst(str) {
|
|
166
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = IntegrationResourceResolver;
|