@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.
Files changed (32) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/domains/database/aurora-builder.js +234 -57
  3. package/infrastructure/domains/database/aurora-builder.test.js +7 -2
  4. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  5. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  6. package/infrastructure/domains/database/migration-builder.js +256 -215
  7. package/infrastructure/domains/database/migration-builder.test.js +5 -111
  8. package/infrastructure/domains/database/migration-resolver.js +163 -0
  9. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  10. package/infrastructure/domains/integration/integration-builder.js +258 -84
  11. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  12. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  13. package/infrastructure/domains/networking/vpc-builder.js +856 -135
  14. package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
  15. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  16. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  17. package/infrastructure/domains/security/kms-builder.js +179 -22
  18. package/infrastructure/domains/security/kms-resolver.js +96 -0
  19. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  20. package/infrastructure/domains/shared/base-resolver.js +186 -0
  21. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  22. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  23. package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
  24. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  25. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  26. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  27. package/infrastructure/domains/shared/types/index.js +46 -0
  28. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  29. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  30. package/package.json +6 -6
  31. package/infrastructure/REFACTOR.md +0 -532
  32. 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
- const functionPackageConfig = {
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
- for (const integration of appDefinition.integrations) {
123
- const integrationName = integration.Definition.name;
124
- const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)}Queue`;
125
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
126
-
127
- console.log(` Adding integration: ${integrationName}`);
128
-
129
- // Add webhook handler if enabled (BEFORE catch-all proxy route)
130
- // CRITICAL: Webhook routes must be defined before the catch-all {proxy+} route
131
- // to ensure proper route matching in AWS API Gateway/HTTP API
132
- const webhookConfig = integration.Definition.webhooks;
133
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
134
- const webhookFunctionName = `${integrationName}Webhook`;
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
- // Create HTTP API handler for integration (catch-all route AFTER webhooks)
159
- result.functions[integrationName] = {
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/{proxy+}`,
167
- method: 'ANY',
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
- // Create SQS Queue for integration
174
- result.resources[queueReference] = {
175
- Type: 'AWS::SQS::Queue',
176
- Properties: {
177
- QueueName: `\${self:custom.${queueReference}}`,
178
- MessageRetentionPeriod: 60,
179
- VisibilityTimeout: 1800,
180
- RedrivePolicy: {
181
- maxReceiveCount: 1,
182
- deadLetterTargetArn: {
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
- // Create Queue Worker function
190
- const queueWorkerName = `${integrationName}QueueWorker`;
191
- result.functions[queueWorkerName] = {
192
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
193
- skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
194
- package: functionPackageConfig,
195
- reservedConcurrency: 5,
196
- events: [
197
- {
198
- sqs: {
199
- arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
200
- batchSize: 1,
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
- timeout: 900, // 15 minutes max for queue workers (Lambda maximum)
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
- // Add queue URL to environment
208
- result.environment[`${integrationName.toUpperCase()}_QUEUE_URL`] = {
209
- Ref: queueReference,
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
- result.custom[queueReference] = queueName;
213
- }
333
+ console.log(' ✓ Created InternalErrorQueue resource');
334
+ }
214
335
 
215
- console.log(` ✅ Configured ${appDefinition.integrations.length} integrations`);
216
- console.log(`[${this.name}] Integration configuration completed`);
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
- return result;
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;