@friggframework/devtools 2.0.0--canary.461.8cf93ae.0 → 2.0.0--canary.474.213c7d9.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,22 +1,25 @@
1
1
  /**
2
2
  * Aurora PostgreSQL Builder
3
- *
3
+ *
4
4
  * Domain Layer - Hexagonal Architecture
5
- *
5
+ *
6
6
  * Responsible for:
7
7
  * - Aurora Serverless v2 cluster creation or discovery
8
8
  * - Database subnet groups
9
9
  * - Database security groups
10
10
  * - Secrets Manager integration for credentials
11
11
  * - Database connection environment variables
12
- *
13
- * Supports three management modes:
14
- * 1. managed: Creates new Aurora cluster
15
- * 2. use-existing: Uses explicitly provided cluster
16
- * 3. discover (default): Discovers existing cluster
12
+ *
13
+ * Uses ownership-based architecture:
14
+ * - STACK: Resources in our CloudFormation template (definitions + Refs)
15
+ * - EXTERNAL: Resources outside our stack (reference by physical ID)
16
+ * - AUTO: System decides based on discovery
17
17
  */
18
18
 
19
19
  const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
20
+ const AuroraResourceResolver = require('./aurora-resolver');
21
+ const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
22
+ const { ResourceOwnership } = require('../shared/types/resource-ownership');
20
23
 
21
24
  class AuroraBuilder extends InfrastructureBuilder {
22
25
  constructor() {
@@ -77,86 +80,260 @@ class AuroraBuilder extends InfrastructureBuilder {
77
80
  }
78
81
 
79
82
  /**
80
- * Build Aurora infrastructure
83
+ * Build Aurora infrastructure using ownership-based architecture
81
84
  */
82
85
  async build(appDefinition, discoveredResources) {
83
86
  console.log(`\n[${this.name}] Configuring Aurora PostgreSQL...`);
84
87
 
85
- const dbConfig = appDefinition.database.postgres;
88
+ // Backwards compatibility: Translate old schema to new ownership schema
89
+ appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
86
90
 
87
- // Normalize top-level managementMode
88
- const globalMode = appDefinition.managementMode || 'discover';
89
- const vpcIsolation = appDefinition.vpcIsolation || 'shared';
91
+ // Initialize result
92
+ const result = {
93
+ resources: {},
94
+ iamStatements: [],
95
+ environment: {},
96
+ };
97
+
98
+ // Special case: use-existing with endpoint (bypass resolver)
99
+ if (appDefinition.database?.postgres?._useExistingEndpoint) {
100
+ console.log(' Using provided database endpoint (use-existing mode)');
101
+ await this.useExistingAurora(appDefinition, discoveredResources, result);
102
+ console.log(`\n[${this.name}] ✅ Aurora PostgreSQL configuration completed`);
103
+ return result;
104
+ }
105
+
106
+ // Get structured discovery result
107
+ const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
108
+
109
+ // Use AuroraResourceResolver to make ownership decisions
110
+ const resolver = new AuroraResourceResolver();
111
+ const decisions = resolver.resolveAll(appDefinition, discovery);
112
+
113
+ console.log('\n 📋 Resource Ownership Decisions:');
114
+ console.log(` Cluster: ${decisions.cluster.ownership} - ${decisions.cluster.reason}`);
115
+ console.log(` Instance: ${decisions.instance.ownership} - ${decisions.instance.reason}`);
116
+ console.log(` Subnet Group: ${decisions.subnetGroup.ownership} - ${decisions.subnetGroup.reason}`);
117
+ console.log(` Secret: ${decisions.secret.ownership} - ${decisions.secret.reason}`);
118
+
119
+ // Build resources based on ownership decisions
120
+ await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
121
+
122
+ console.log(`\n[${this.name}] ✅ Aurora PostgreSQL configuration completed`);
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Convert flat discovery to structured discovery
128
+ * Provides backwards compatibility for tests
129
+ */
130
+ convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
131
+ const discovery = createEmptyDiscoveryResult();
132
+
133
+ if (!flatDiscovery) {
134
+ return discovery;
135
+ }
136
+
137
+ // Check if resources are from CloudFormation stack
138
+ const isManagedIsolated = appDefinition.managementMode === 'managed' &&
139
+ (appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
140
+ const hasExistingStackResources = isManagedIsolated && flatDiscovery.auroraClusterId &&
141
+ typeof flatDiscovery.auroraClusterId === 'string';
142
+
143
+ if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
144
+ discovery.fromCloudFormation = true;
145
+ discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
146
+
147
+ // Add stack-managed resources
148
+ let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
149
+
150
+ // Infer logical IDs from physical IDs if needed
151
+ if (hasExistingStackResources && existingLogicalIds.length === 0) {
152
+ if (flatDiscovery.auroraClusterId) existingLogicalIds.push('FriggAuroraCluster');
153
+ if (flatDiscovery.auroraInstanceId) existingLogicalIds.push('FriggAuroraInstance');
154
+ if (flatDiscovery.dbSubnetGroupName) existingLogicalIds.push('FriggDBSubnetGroup');
155
+ if (flatDiscovery.dbSecretArn) existingLogicalIds.push('FriggDBSecret');
156
+ }
157
+
158
+ existingLogicalIds.forEach(logicalId => {
159
+ let resourceType = '';
160
+ let physicalId = '';
161
+
162
+ if (logicalId === 'FriggAuroraCluster') {
163
+ resourceType = 'AWS::RDS::DBCluster';
164
+ physicalId = flatDiscovery.auroraClusterId;
165
+ } else if (logicalId === 'FriggAuroraInstance') {
166
+ resourceType = 'AWS::RDS::DBInstance';
167
+ physicalId = flatDiscovery.auroraInstanceId;
168
+ } else if (logicalId === 'FriggDBSubnetGroup') {
169
+ resourceType = 'AWS::RDS::DBSubnetGroup';
170
+ physicalId = flatDiscovery.dbSubnetGroupName;
171
+ } else if (logicalId === 'FriggDBSecret') {
172
+ resourceType = 'AWS::SecretsManager::Secret';
173
+ physicalId = flatDiscovery.dbSecretArn;
174
+ }
175
+
176
+ if (physicalId && typeof physicalId === 'string') {
177
+ discovery.stackManaged.push({
178
+ logicalId,
179
+ physicalId,
180
+ resourceType
181
+ });
182
+ }
183
+ });
184
+ } else {
185
+ // Resources discovered from AWS API (external)
186
+ // Handle both cluster ID and endpoint
187
+ if (flatDiscovery.auroraClusterId && typeof flatDiscovery.auroraClusterId === 'string') {
188
+ discovery.external.push({
189
+ physicalId: flatDiscovery.auroraClusterId,
190
+ resourceType: 'AWS::RDS::DBCluster',
191
+ source: 'aws-discovery'
192
+ });
193
+ } else if (flatDiscovery.auroraClusterEndpoint && typeof flatDiscovery.auroraClusterEndpoint === 'string') {
194
+ // Endpoint provided (discover mode) - treat as external
195
+ discovery.external.push({
196
+ physicalId: flatDiscovery.auroraClusterEndpoint,
197
+ resourceType: 'AWS::RDS::DBCluster',
198
+ source: 'aws-discovery',
199
+ properties: { Endpoint: flatDiscovery.auroraClusterEndpoint }
200
+ });
201
+ }
202
+
203
+ if (flatDiscovery.auroraInstanceId && typeof flatDiscovery.auroraInstanceId === 'string') {
204
+ discovery.external.push({
205
+ physicalId: flatDiscovery.auroraInstanceId,
206
+ resourceType: 'AWS::RDS::DBInstance',
207
+ source: 'aws-discovery'
208
+ });
209
+ }
210
+ }
90
211
 
91
- // Debug logging
92
- console.log(` 🔍 DEBUG: Aurora globalMode = '${globalMode}', vpcIsolation = '${vpcIsolation}'`);
93
- console.log(` 🔍 DEBUG: Aurora discoveredResources.auroraClusterId = ${discoveredResources?.auroraClusterId}`);
212
+ return discovery;
213
+ }
94
214
 
95
- let management = dbConfig.management;
215
+ /**
216
+ * Translate legacy configuration to ownership-based configuration
217
+ * Provides backwards compatibility
218
+ */
219
+ translateLegacyConfig(appDefinition, discoveredResources) {
220
+ // If already using ownership schema, return as-is
221
+ if (appDefinition.database?.postgres?.ownership) {
222
+ return appDefinition;
223
+ }
224
+
225
+ const translated = JSON.parse(JSON.stringify(appDefinition));
226
+
227
+ // Initialize ownership sections
228
+ if (!translated.database) translated.database = {};
229
+ if (!translated.database.postgres) translated.database.postgres = {};
230
+ if (!translated.database.postgres.ownership) {
231
+ translated.database.postgres.ownership = {};
232
+ }
233
+ if (!translated.database.postgres.external) {
234
+ translated.database.postgres.external = {};
235
+ }
236
+ if (!translated.database.postgres.config) {
237
+ translated.database.postgres.config = {};
238
+ }
239
+
240
+ // Handle top-level managementMode
241
+ const globalMode = appDefinition.managementMode || 'discover';
242
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
96
243
 
97
244
  if (globalMode === 'managed') {
98
- // Warn about ignored granular options
99
- if (dbConfig.management) {
245
+ if (appDefinition.database?.postgres?.management) {
100
246
  console.log(` ⚠️ managementMode='managed' ignoring: database.postgres.management`);
101
247
  }
102
248
 
103
- // Clear granular option to prevent conflicts
104
- delete appDefinition.database.postgres.management;
105
-
106
- // Set management based on isolation strategy AND existing stack resources
107
249
  if (vpcIsolation === 'isolated') {
108
- // Check if CloudFormation stack already has Aurora (stage-specific)
109
- // CloudFormation discovery sets 'auroraClusterId' (string) when found in stack
110
250
  const hasStackAurora = discoveredResources?.auroraClusterId &&
111
251
  typeof discoveredResources.auroraClusterId === 'string';
112
252
 
113
- console.log(` 🔍 DEBUG: Aurora hasStackAurora = ${hasStackAurora}`);
114
-
115
253
  if (hasStackAurora) {
116
- // Stack has Aurora - reuse it (standard flow: stack → orphaned → create)
117
- management = 'discover';
118
- appDefinition.database.postgres.autoCreateCredentials = true;
254
+ translated.database.postgres.ownership.cluster = 'auto';
255
+ translated.database.postgres.ownership.instance = 'auto';
256
+ translated.database.postgres.ownership.subnetGroup = 'auto';
257
+ translated.database.postgres.ownership.secret = 'auto';
119
258
  console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has Aurora, reusing`);
120
259
  } else {
121
- // No stack Aurora - create new isolated Aurora for this stage
122
- management = 'managed';
260
+ translated.database.postgres.ownership.cluster = 'stack';
261
+ translated.database.postgres.ownership.instance = 'stack';
262
+ translated.database.postgres.ownership.subnetGroup = 'stack';
263
+ translated.database.postgres.ownership.secret = 'stack';
123
264
  console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack Aurora, creating new`);
124
265
  }
125
266
  } else {
126
- management = 'discover'; // Shared VPC = reuse Aurora
127
- appDefinition.database.postgres.autoCreateCredentials = true;
267
+ translated.database.postgres.ownership.cluster = 'auto';
268
+ translated.database.postgres.ownership.instance = 'auto';
269
+ translated.database.postgres.ownership.subnetGroup = 'auto';
270
+ translated.database.postgres.ownership.secret = 'auto';
128
271
  console.log(` managementMode='managed' + vpcIsolation='shared' → discovering Aurora`);
129
272
  }
130
273
  } else if (globalMode === 'existing') {
131
- management = 'existing';
132
- } else {
133
- management = management || 'discover';
274
+ translated.database.postgres.ownership.cluster = 'external';
275
+ translated.database.postgres.ownership.instance = 'external';
134
276
  }
135
277
 
136
- console.log(` PostgreSQL Management Mode: ${management}`);
137
-
138
- const result = {
139
- resources: {},
140
- iamStatements: [],
141
- environment: {},
142
- };
278
+ // Handle legacy database.postgres.management
279
+ // BUT: if managementMode (top-level) is set, it takes precedence
280
+ const dbManagement = appDefinition.database?.postgres?.management;
281
+ if (dbManagement && globalMode !== 'managed' && globalMode !== 'existing') {
282
+ if (dbManagement === 'managed') {
283
+ translated.database.postgres.ownership.cluster = 'stack';
284
+ translated.database.postgres.ownership.instance = 'stack';
285
+ translated.database.postgres.ownership.subnetGroup = 'stack';
286
+ translated.database.postgres.ownership.secret = 'stack';
287
+ } else if (dbManagement === 'use-existing') {
288
+ // For use-existing with endpoint, we bypass resolver entirely
289
+ // Mark this with a special flag
290
+ translated.database.postgres._useExistingEndpoint = true;
291
+ if (appDefinition.database.postgres.endpoint) {
292
+ translated.database.postgres.external.endpoint = appDefinition.database.postgres.endpoint;
293
+ }
294
+ } else if (dbManagement === 'discover') {
295
+ translated.database.postgres.ownership.cluster = 'auto';
296
+ translated.database.postgres.ownership.instance = 'auto';
297
+ }
298
+ }
143
299
 
144
- // Handle different management modes
145
- switch (management) {
146
- case 'managed':
147
- await this.createNewAurora(appDefinition, discoveredResources, result);
148
- break;
149
- case 'use-existing':
150
- await this.useExistingAurora(appDefinition, discoveredResources, result);
151
- break;
152
- case 'discover':
153
- default:
154
- await this.discoverAurora(appDefinition, discoveredResources, result);
155
- break;
300
+ // Preserve other database config
301
+ if (appDefinition.database?.postgres?.minCapacity) {
302
+ translated.database.postgres.config.minCapacity = appDefinition.database.postgres.minCapacity;
303
+ }
304
+ if (appDefinition.database?.postgres?.maxCapacity) {
305
+ translated.database.postgres.config.maxCapacity = appDefinition.database.postgres.maxCapacity;
306
+ }
307
+ if (appDefinition.database?.postgres?.publiclyAccessible !== undefined) {
308
+ translated.database.postgres.config.publiclyAccessible = appDefinition.database.postgres.publiclyAccessible;
156
309
  }
157
310
 
158
- console.log(`[${this.name}] ✅ Aurora PostgreSQL configuration completed`);
159
- return result;
311
+ return translated;
312
+ }
313
+
314
+ /**
315
+ * Build all Aurora resources based on ownership decisions
316
+ */
317
+ async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
318
+ // Determine build strategy from ownership decisions
319
+
320
+ if (decisions.cluster.ownership === ResourceOwnership.EXTERNAL) {
321
+ // External cluster discovered - reference it without creating infrastructure
322
+ console.log(' → Discovering and referencing external Aurora cluster');
323
+ await this.discoverAurora(appDefinition, discoveredResources, result);
324
+ } else if (decisions.cluster.ownership === ResourceOwnership.STACK && decisions.cluster.physicalId) {
325
+ // Cluster exists in stack - add definitions (CloudFormation idempotency)
326
+ console.log(' → Adding Aurora definitions to template (existing in stack)');
327
+ await this.createNewAurora(appDefinition, discoveredResources, result);
328
+ } else if (decisions.cluster.ownership === ResourceOwnership.STACK && !decisions.cluster.physicalId) {
329
+ // Create new cluster (stack, no existing)
330
+ console.log(' → Creating new Aurora cluster in stack');
331
+ await this.createNewAurora(appDefinition, discoveredResources, result);
332
+ } else {
333
+ // Fallback: discover mode
334
+ console.log(' → Discovering Aurora resources');
335
+ await this.discoverAurora(appDefinition, discoveredResources, result);
336
+ }
160
337
  }
161
338
 
162
339
  /**
@@ -788,8 +788,12 @@ describe('AuroraBuilder', () => {
788
788
  expect.stringContaining("stack has Aurora, reusing")
789
789
  );
790
790
 
791
- // Should REUSE stack Aurora (not create new)
792
- expect(result.resources.FriggAuroraCluster).toBeUndefined();
791
+ // Should keep Aurora definitions in template (CloudFormation idempotency)
792
+ // Even though Aurora exists in stack, we include definitions - CF won't recreate
793
+ expect(result.resources.FriggAuroraCluster).toBeDefined();
794
+ expect(result.resources.FriggAuroraCluster.Type).toBe('AWS::RDS::DBCluster');
795
+ expect(result.resources.FriggAuroraInstance).toBeDefined();
796
+ expect(result.resources.FriggAuroraInstance.Type).toBe('AWS::RDS::DBInstance');
793
797
  expect(result.environment.DATABASE_URL).toBeDefined();
794
798
 
795
799
  consoleLogSpy.mockRestore();
@@ -853,6 +857,7 @@ describe('AuroraBuilder', () => {
853
857
  auroraClusterEndpoint: 'existing-cluster.us-east-1.rds.amazonaws.com',
854
858
  auroraClusterPort: 5432,
855
859
  auroraClusterIdentifier: 'existing-cluster',
860
+ databaseSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:shared-db-secret',
856
861
  privateSubnetId1: 'subnet-1',
857
862
  privateSubnetId2: 'subnet-2',
858
863
  };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Aurora Resource Resolver
3
+ *
4
+ * Resolves ownership for Aurora PostgreSQL resources following the ownership-based architecture.
5
+ *
6
+ * Resources managed:
7
+ * - Aurora Cluster (RDS::DBCluster)
8
+ * - Aurora Instance (RDS::DBInstance)
9
+ * - DB Subnet Group (RDS::DBSubnetGroup)
10
+ * - DB Secret (SecretsManager::Secret)
11
+ *
12
+ * Ownership types:
13
+ * - STACK: Resource defined in our CloudFormation template (use Refs)
14
+ * - EXTERNAL: Resource outside our stack (use physical IDs)
15
+ * - AUTO: System decides based on discovery
16
+ */
17
+
18
+ const BaseResourceResolver = require('../shared/base-resolver');
19
+ const { ResourceOwnership } = require('../shared/types/resource-ownership');
20
+
21
+ class AuroraResourceResolver extends BaseResourceResolver {
22
+ /**
23
+ * Resolve Aurora Cluster ownership
24
+ * @param {Object} appDefinition - App definition
25
+ * @param {Object} discovery - Discovery result
26
+ * @returns {Object} Resource decision
27
+ */
28
+ resolveCluster(appDefinition, discovery) {
29
+ const userIntent = appDefinition.database?.postgres?.ownership?.cluster || 'auto';
30
+
31
+ // Explicit external - use provided cluster identifier
32
+ if (userIntent === 'external') {
33
+ this.requireExternalIds(
34
+ appDefinition.database?.postgres?.external?.clusterIdentifier,
35
+ 'clusterIdentifier'
36
+ );
37
+ return this.createExternalDecision(
38
+ appDefinition.database.postgres.external.clusterIdentifier,
39
+ 'User specified ownership=external for Aurora cluster'
40
+ );
41
+ }
42
+
43
+ // For stack or auto: check if cluster exists in stack
44
+ const inStack = this.findInStack('FriggAuroraCluster', discovery);
45
+
46
+ if (inStack) {
47
+ return this.createStackDecision(
48
+ inStack.physicalId,
49
+ 'Found FriggAuroraCluster in CloudFormation stack'
50
+ );
51
+ }
52
+
53
+ // Check for external cluster
54
+ const external = this.findExternal('AWS::RDS::DBCluster', discovery);
55
+ if (external && userIntent === 'auto') {
56
+ return this.createExternalDecision(
57
+ external.physicalId,
58
+ 'Found external Aurora cluster via discovery'
59
+ );
60
+ }
61
+
62
+ // Create new cluster in stack
63
+ return this.createStackDecision(
64
+ null,
65
+ 'No existing Aurora cluster - will create in stack'
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Resolve Aurora Instance ownership
71
+ * @param {Object} appDefinition - App definition
72
+ * @param {Object} discovery - Discovery result
73
+ * @returns {Object} Resource decision
74
+ */
75
+ resolveInstance(appDefinition, discovery) {
76
+ const userIntent = appDefinition.database?.postgres?.ownership?.instance || 'auto';
77
+
78
+ // Explicit external
79
+ if (userIntent === 'external') {
80
+ this.requireExternalIds(
81
+ appDefinition.database?.postgres?.external?.instanceIdentifier,
82
+ 'instanceIdentifier'
83
+ );
84
+ return this.createExternalDecision(
85
+ appDefinition.database.postgres.external.instanceIdentifier,
86
+ 'User specified ownership=external for Aurora instance'
87
+ );
88
+ }
89
+
90
+ // Check if instance exists in stack
91
+ const inStack = this.findInStack('FriggAuroraInstance', discovery);
92
+
93
+ if (inStack) {
94
+ return this.createStackDecision(
95
+ inStack.physicalId,
96
+ 'Found FriggAuroraInstance in CloudFormation stack'
97
+ );
98
+ }
99
+
100
+ // Check for external instance
101
+ const external = this.findExternal('AWS::RDS::DBInstance', discovery);
102
+ if (external && userIntent === 'auto') {
103
+ return this.createExternalDecision(
104
+ external.physicalId,
105
+ 'Found external Aurora instance via discovery'
106
+ );
107
+ }
108
+
109
+ // Create new instance in stack
110
+ return this.createStackDecision(
111
+ null,
112
+ 'No existing Aurora instance - will create in stack'
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Resolve DB Subnet Group ownership
118
+ * @param {Object} appDefinition - App definition
119
+ * @param {Object} discovery - Discovery result
120
+ * @returns {Object} Resource decision
121
+ */
122
+ resolveSubnetGroup(appDefinition, discovery) {
123
+ const userIntent = appDefinition.database?.postgres?.ownership?.subnetGroup || 'auto';
124
+
125
+ // Explicit external
126
+ if (userIntent === 'external') {
127
+ this.requireExternalIds(
128
+ appDefinition.database?.postgres?.external?.subnetGroupName,
129
+ 'subnetGroupName'
130
+ );
131
+ return this.createExternalDecision(
132
+ appDefinition.database.postgres.external.subnetGroupName,
133
+ 'User specified ownership=external for DB subnet group'
134
+ );
135
+ }
136
+
137
+ // Check if subnet group exists in stack
138
+ const inStack = this.findInStack('FriggDBSubnetGroup', discovery);
139
+
140
+ if (inStack) {
141
+ return this.createStackDecision(
142
+ inStack.physicalId,
143
+ 'Found FriggDBSubnetGroup in CloudFormation stack'
144
+ );
145
+ }
146
+
147
+ // For subnet groups, always create in stack (they're cheap and cluster-specific)
148
+ return this.createStackDecision(
149
+ null,
150
+ 'No existing DB subnet group - will create in stack'
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Resolve DB Secret ownership
156
+ * @param {Object} appDefinition - App definition
157
+ * @param {Object} discovery - Discovery result
158
+ * @returns {Object} Resource decision
159
+ */
160
+ resolveSecret(appDefinition, discovery) {
161
+ const userIntent = appDefinition.database?.postgres?.ownership?.secret || 'auto';
162
+
163
+ // Explicit external - use provided secret ARN
164
+ if (userIntent === 'external') {
165
+ this.requireExternalIds(
166
+ appDefinition.database?.postgres?.external?.secretArn,
167
+ 'secretArn'
168
+ );
169
+ return this.createExternalDecision(
170
+ appDefinition.database.postgres.external.secretArn,
171
+ 'User specified ownership=external for DB secret'
172
+ );
173
+ }
174
+
175
+ // Check if secret exists in stack
176
+ const inStack = this.findInStack('FriggDBSecret', discovery);
177
+
178
+ if (inStack) {
179
+ return this.createStackDecision(
180
+ inStack.physicalId,
181
+ 'Found FriggDBSecret in CloudFormation stack'
182
+ );
183
+ }
184
+
185
+ // For secrets tied to the cluster, always create in stack
186
+ return this.createStackDecision(
187
+ null,
188
+ 'No existing DB secret - will create in stack'
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Resolve all Aurora resources at once
194
+ * Convenience method that returns decisions for all Aurora resources
195
+ *
196
+ * @param {Object} appDefinition - App definition
197
+ * @param {Object} discovery - Discovery result
198
+ * @returns {Object} Decisions for all Aurora resources
199
+ */
200
+ resolveAll(appDefinition, discovery) {
201
+ return {
202
+ cluster: this.resolveCluster(appDefinition, discovery),
203
+ instance: this.resolveInstance(appDefinition, discovery),
204
+ subnetGroup: this.resolveSubnetGroup(appDefinition, discovery),
205
+ secret: this.resolveSecret(appDefinition, discovery)
206
+ };
207
+ }
208
+ }
209
+
210
+ module.exports = AuroraResourceResolver;