@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
@@ -0,0 +1,334 @@
1
+ /**
2
+ * CloudFormation-based Resource Discovery v2
3
+ *
4
+ * Refactored to return structured DiscoveryResult instead of flat object.
5
+ * Part of the clean resource ownership architecture.
6
+ *
7
+ * Domain Service - Hexagonal Architecture
8
+ */
9
+
10
+ const { createEmptyDiscoveryResult } = require('./types');
11
+
12
+ class CloudFormationDiscoveryV2 {
13
+ constructor(provider, config = {}) {
14
+ this.provider = provider;
15
+ this.serviceName = config.serviceName;
16
+ this.stage = config.stage;
17
+ }
18
+
19
+ /**
20
+ * Discover resources from an existing CloudFormation stack
21
+ *
22
+ * @param {string} stackName - Name of the CloudFormation stack
23
+ * @returns {Promise<Object>} DiscoveryResult or empty result if stack doesn't exist
24
+ */
25
+ async discoverFromStack(stackName) {
26
+ try {
27
+ // Try to get the stack
28
+ const stack = await this.provider.describeStack(stackName);
29
+
30
+ // Get stack resources
31
+ const resources = await this.provider.listStackResources(stackName);
32
+
33
+ // Create structured discovery result
34
+ const discovery = createEmptyDiscoveryResult();
35
+ discovery.fromCloudFormation = true;
36
+ discovery.stackName = stackName;
37
+ discovery.region = this.provider.region;
38
+
39
+ // Extract stack-managed resources
40
+ await this._extractStackManagedResources(resources || [], discovery);
41
+
42
+ // Also keep flat structure for backwards compatibility (temporarily)
43
+ const flatDiscovered = {
44
+ fromCloudFormationStack: true,
45
+ stackName: stackName,
46
+ existingLogicalIds: discovery.stackManaged.map(r => r.logicalId)
47
+ };
48
+
49
+ // Extract from outputs (legacy)
50
+ if (stack.Outputs && stack.Outputs.length > 0) {
51
+ this._extractFromOutputs(stack.Outputs, flatDiscovered);
52
+ }
53
+
54
+ // Extract flat properties from stackManaged resources
55
+ this._createFlatPropertiesFromStackManaged(discovery, flatDiscovered);
56
+
57
+ // Return both structures (flat for backwards compat, structured for new code)
58
+ return {
59
+ ...flatDiscovered,
60
+ _structured: discovery // New structured format
61
+ };
62
+ } catch (error) {
63
+ // Stack doesn't exist - return empty discovery
64
+ if (error.message && error.message.includes('does not exist')) {
65
+ const empty = createEmptyDiscoveryResult();
66
+ return {
67
+ fromCloudFormationStack: false,
68
+ _structured: empty
69
+ };
70
+ }
71
+
72
+ // Other errors - log and return empty
73
+ console.warn(`⚠️ CloudFormation discovery failed: ${error.message}`);
74
+ const empty = createEmptyDiscoveryResult();
75
+ return {
76
+ fromCloudFormationStack: false,
77
+ _structured: empty
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Extract stack-managed resources into structured format
84
+ * @private
85
+ */
86
+ async _extractStackManagedResources(resources, discovery) {
87
+ console.log(` DEBUG: Processing ${resources.length} CloudFormation resources...`);
88
+
89
+ for (const resource of resources) {
90
+ const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
91
+
92
+ // Only track Frigg-managed resources
93
+ if (!LogicalResourceId.startsWith('Frigg') && !LogicalResourceId.includes('Migration')) {
94
+ continue;
95
+ }
96
+
97
+ // Add to stack-managed list
98
+ const stackResource = {
99
+ logicalId: LogicalResourceId,
100
+ physicalId: PhysicalResourceId,
101
+ resourceType: ResourceType,
102
+ properties: {} // Will be populated as needed
103
+ };
104
+
105
+ discovery.stackManaged.push(stackResource);
106
+
107
+ // Query AWS for detailed properties for certain resources
108
+ await this._enrichResourceProperties(stackResource, discovery);
109
+ }
110
+
111
+ console.log(` ✓ Discovered ${discovery.stackManaged.length} stack-managed resources`);
112
+ }
113
+
114
+ /**
115
+ * Enrich resource properties by querying AWS APIs
116
+ * @private
117
+ */
118
+ async _enrichResourceProperties(stackResource, discovery) {
119
+ const { logicalId, physicalId, resourceType } = stackResource;
120
+
121
+ // Security Group - query to get VPC ID
122
+ if (logicalId === 'FriggLambdaSecurityGroup' && resourceType === 'AWS::EC2::SecurityGroup') {
123
+ console.log(` ✓ Found security group in stack: ${physicalId}`);
124
+
125
+ if (this.provider && this.provider.getEC2Client) {
126
+ try {
127
+ console.log(` Querying EC2 to get VPC ID from security group...`);
128
+ const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
129
+ const ec2Client = this.provider.getEC2Client();
130
+ const sgDetails = await ec2Client.send(
131
+ new DescribeSecurityGroupsCommand({
132
+ GroupIds: [physicalId]
133
+ })
134
+ );
135
+
136
+ if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
137
+ const sg = sgDetails.SecurityGroups[0];
138
+ stackResource.properties.VpcId = sg.VpcId;
139
+ console.log(` ✓ Extracted VPC ID from security group: ${sg.VpcId}`);
140
+
141
+ // Also add VPC to stack-managed if not already there
142
+ const vpcExists = discovery.stackManaged.some(r => r.logicalId === 'FriggVPC');
143
+ if (!vpcExists && sg.VpcId) {
144
+ // Note: VPC was created by stack, just not listed in resources yet
145
+ // We'll add it when we encounter it, or infer it here
146
+ }
147
+ }
148
+ } catch (error) {
149
+ console.warn(` ⚠️ Could not get VPC from security group: ${error.message}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Aurora Cluster - query to get endpoint
155
+ if (logicalId === 'FriggAuroraCluster' && resourceType === 'AWS::RDS::DBCluster') {
156
+ console.log(` ✓ Found Aurora cluster in stack: ${physicalId}`);
157
+
158
+ if (this.provider) {
159
+ try {
160
+ console.log(` Querying RDS to get Aurora endpoint...`);
161
+ const { DescribeDBClustersCommand, RDSClient } = require('@aws-sdk/client-rds');
162
+
163
+ const rdsClient = new RDSClient({ region: this.provider.region });
164
+ const clusterDetails = await rdsClient.send(
165
+ new DescribeDBClustersCommand({
166
+ DBClusterIdentifier: physicalId
167
+ })
168
+ );
169
+
170
+ if (clusterDetails.DBClusters && clusterDetails.DBClusters.length > 0) {
171
+ const cluster = clusterDetails.DBClusters[0];
172
+ stackResource.properties.Endpoint = cluster.Endpoint;
173
+ stackResource.properties.Port = cluster.Port;
174
+ stackResource.properties.DBClusterIdentifier = cluster.DBClusterIdentifier;
175
+ console.log(` ✓ Extracted Aurora endpoint: ${cluster.Endpoint}:${cluster.Port}`);
176
+ }
177
+ } catch (error) {
178
+ console.warn(` ⚠️ Could not get endpoint from Aurora cluster: ${error.message}`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // KMS Key Alias - query to get ARN
184
+ if (logicalId === 'FriggKMSKeyAlias' && resourceType === 'AWS::KMS::Alias') {
185
+ console.log(` ✓ Found KMS key alias in stack: ${physicalId}`);
186
+
187
+ if (this.provider && this.provider.describeKmsKey) {
188
+ try {
189
+ console.log(` Querying KMS for alias: ${physicalId}...`);
190
+ const keyMetadata = await this.provider.describeKmsKey(physicalId);
191
+
192
+ if (keyMetadata) {
193
+ stackResource.properties.KeyArn = keyMetadata.Arn;
194
+ console.log(` ✓ Found KMS key via alias query: ${keyMetadata.Arn}`);
195
+ }
196
+ } catch (error) {
197
+ console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ // VPC - just log
203
+ if (logicalId === 'FriggVPC' && resourceType === 'AWS::EC2::VPC') {
204
+ console.log(` ✓ Found VPC in stack: ${physicalId}`);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Create flat properties from stack-managed resources (backwards compatibility)
210
+ * @private
211
+ */
212
+ _createFlatPropertiesFromStackManaged(discovery, flatDiscovered) {
213
+ for (const resource of discovery.stackManaged) {
214
+ const { logicalId, physicalId, properties } = resource;
215
+
216
+ // Map to flat property names
217
+ switch (logicalId) {
218
+ case 'FriggVPC':
219
+ flatDiscovered.defaultVpcId = physicalId;
220
+ break;
221
+ case 'FriggLambdaSecurityGroup':
222
+ flatDiscovered.securityGroupId = physicalId;
223
+ if (properties.VpcId) {
224
+ flatDiscovered.defaultVpcId = properties.VpcId;
225
+ }
226
+ break;
227
+ case 'FriggPrivateSubnet1':
228
+ flatDiscovered.privateSubnetId1 = physicalId;
229
+ break;
230
+ case 'FriggPrivateSubnet2':
231
+ flatDiscovered.privateSubnetId2 = physicalId;
232
+ break;
233
+ case 'FriggPublicSubnet':
234
+ flatDiscovered.publicSubnetId1 = physicalId;
235
+ break;
236
+ case 'FriggPublicSubnet2':
237
+ flatDiscovered.publicSubnetId2 = physicalId;
238
+ break;
239
+ case 'FriggNatGateway':
240
+ flatDiscovered.natGatewayId = physicalId;
241
+ break;
242
+ case 'FriggLambdaRouteTable':
243
+ flatDiscovered.routeTableId = physicalId;
244
+ break;
245
+ case 'FriggVPCEndpointSecurityGroup':
246
+ flatDiscovered.vpcEndpointSecurityGroupId = physicalId;
247
+ break;
248
+ case 'FriggS3VPCEndpoint':
249
+ flatDiscovered.s3VpcEndpointId = physicalId;
250
+ break;
251
+ case 'FriggDynamoDBVPCEndpoint':
252
+ flatDiscovered.dynamoDbVpcEndpointId = physicalId;
253
+ break;
254
+ case 'FriggKMSVPCEndpoint':
255
+ flatDiscovered.kmsVpcEndpointId = physicalId;
256
+ break;
257
+ case 'FriggSecretsManagerVPCEndpoint':
258
+ flatDiscovered.secretsManagerVpcEndpointId = physicalId;
259
+ break;
260
+ case 'FriggSQSVPCEndpoint':
261
+ flatDiscovered.sqsVpcEndpointId = physicalId;
262
+ break;
263
+ case 'FriggAuroraCluster':
264
+ flatDiscovered.auroraClusterId = physicalId;
265
+ if (properties.Endpoint) {
266
+ flatDiscovered.auroraClusterEndpoint = properties.Endpoint;
267
+ }
268
+ if (properties.Port) {
269
+ flatDiscovered.auroraClusterPort = properties.Port;
270
+ flatDiscovered.auroraPort = properties.Port;
271
+ }
272
+ if (properties.DBClusterIdentifier) {
273
+ flatDiscovered.auroraClusterIdentifier = properties.DBClusterIdentifier;
274
+ }
275
+ break;
276
+ case 'FriggKMSKey':
277
+ flatDiscovered.defaultKmsKeyId = physicalId;
278
+ break;
279
+ case 'FriggKMSKeyAlias':
280
+ flatDiscovered.kmsKeyAlias = physicalId;
281
+ if (properties.KeyArn) {
282
+ flatDiscovered.defaultKmsKeyId = properties.KeyArn;
283
+ }
284
+ break;
285
+ case 'FriggMigrationStatusBucket':
286
+ flatDiscovered.migrationStatusBucket = physicalId;
287
+ break;
288
+ case 'DbMigrationQueue':
289
+ flatDiscovered.migrationQueueUrl = physicalId;
290
+ break;
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Extract discovered resources from CloudFormation stack outputs (legacy)
297
+ * @private
298
+ */
299
+ _extractFromOutputs(outputs, discovered) {
300
+ const outputMap = outputs.reduce((acc, output) => {
301
+ acc[output.OutputKey] = output.OutputValue;
302
+ return acc;
303
+ }, {});
304
+
305
+ // VPC outputs
306
+ if (outputMap.VpcId) {
307
+ discovered.defaultVpcId = outputMap.VpcId;
308
+ }
309
+
310
+ if (outputMap.PrivateSubnetIds) {
311
+ discovered.privateSubnetIds = outputMap.PrivateSubnetIds.split(',').map(id => id.trim());
312
+ }
313
+
314
+ if (outputMap.PublicSubnetId) {
315
+ discovered.publicSubnetId = outputMap.PublicSubnetId;
316
+ }
317
+
318
+ if (outputMap.SecurityGroupId) {
319
+ discovered.securityGroupId = outputMap.SecurityGroupId;
320
+ }
321
+
322
+ // KMS outputs
323
+ if (outputMap.KMS_KEY_ARN) {
324
+ discovered.defaultKmsKeyId = outputMap.KMS_KEY_ARN;
325
+ }
326
+
327
+ // Database outputs
328
+ if (outputMap.DatabaseEndpoint) {
329
+ discovered.databaseEndpoint = outputMap.DatabaseEndpoint;
330
+ }
331
+ }
332
+ }
333
+
334
+ module.exports = CloudFormationDiscoveryV2;
@@ -48,6 +48,8 @@ describe('CloudFormationDiscovery', () => {
48
48
  const result = await cfDiscovery.discoverFromStack('test-stack');
49
49
 
50
50
  expect(result).toEqual({
51
+ fromCloudFormationStack: true,
52
+ stackName: 'test-stack',
51
53
  defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId', not 'vpcId'
52
54
  privateSubnetIds: ['subnet-1', 'subnet-2'],
53
55
  publicSubnetId: 'subnet-3',
@@ -69,6 +71,8 @@ describe('CloudFormationDiscovery', () => {
69
71
  const result = await cfDiscovery.discoverFromStack('test-stack');
70
72
 
71
73
  expect(result).toEqual({
74
+ fromCloudFormationStack: true,
75
+ stackName: 'test-stack',
72
76
  defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
73
77
  });
74
78
  });
@@ -137,7 +141,10 @@ describe('CloudFormationDiscovery', () => {
137
141
  const result = await cfDiscovery.discoverFromStack('test-stack');
138
142
 
139
143
  expect(result).toEqual({
144
+ fromCloudFormationStack: true,
145
+ stackName: 'test-stack',
140
146
  auroraClusterId: 'test-cluster',
147
+ existingLogicalIds: ['FriggAuroraCluster'],
141
148
  });
142
149
  });
143
150
 
@@ -201,7 +208,10 @@ describe('CloudFormationDiscovery', () => {
201
208
  const result = await cfDiscovery.discoverFromStack('test-stack');
202
209
 
203
210
  expect(result).toEqual({
211
+ fromCloudFormationStack: true,
212
+ stackName: 'test-stack',
204
213
  migrationStatusBucket: 'test-migration-bucket',
214
+ existingLogicalIds: ['FriggMigrationStatusBucket'],
205
215
  });
206
216
  });
207
217
 
@@ -225,7 +235,10 @@ describe('CloudFormationDiscovery', () => {
225
235
  const result = await cfDiscovery.discoverFromStack('test-stack');
226
236
 
227
237
  expect(result).toEqual({
238
+ fromCloudFormationStack: true,
239
+ stackName: 'test-stack',
228
240
  migrationQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
241
+ existingLogicalIds: ['DbMigrationQueue'],
229
242
  });
230
243
  });
231
244
 
@@ -249,7 +262,10 @@ describe('CloudFormationDiscovery', () => {
249
262
  const result = await cfDiscovery.discoverFromStack('test-stack');
250
263
 
251
264
  expect(result).toEqual({
265
+ fromCloudFormationStack: true,
266
+ stackName: 'test-stack',
252
267
  natGatewayId: 'nat-0123456789',
268
+ existingLogicalIds: ['FriggNatGateway'],
253
269
  });
254
270
  });
255
271
 
@@ -273,7 +289,10 @@ describe('CloudFormationDiscovery', () => {
273
289
  const result = await cfDiscovery.discoverFromStack('test-stack');
274
290
 
275
291
  expect(result).toEqual({
292
+ fromCloudFormationStack: true,
293
+ stackName: 'test-stack',
276
294
  defaultVpcId: 'vpc-037ec55fe87aec1e7',
295
+ existingLogicalIds: ['FriggVPC'],
277
296
  });
278
297
  });
279
298
 
@@ -305,10 +324,13 @@ describe('CloudFormationDiscovery', () => {
305
324
  const result = await cfDiscovery.discoverFromStack('test-stack');
306
325
 
307
326
  expect(result).toEqual({
327
+ fromCloudFormationStack: true,
328
+ stackName: 'test-stack',
308
329
  defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId'
309
330
  defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
310
331
  auroraClusterId: 'test-cluster',
311
332
  natGatewayId: 'nat-123',
333
+ existingLogicalIds: ['FriggAuroraCluster', 'FriggNatGateway'],
312
334
  });
313
335
  });
314
336
 
@@ -329,7 +351,10 @@ describe('CloudFormationDiscovery', () => {
329
351
 
330
352
  const result = await cfDiscovery.discoverFromStack('test-stack');
331
353
 
332
- expect(result).toEqual({});
354
+ expect(result).toEqual({
355
+ fromCloudFormationStack: true,
356
+ stackName: 'test-stack',
357
+ });
333
358
  });
334
359
 
335
360
  it('should query EC2 for subnets when VPC found but no subnet resources in stack', async () => {
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview App definition types
3
+ *
4
+ * Defines the structure of the application definition with new ownership-based schema.
5
+ * This replaces the old 'management' mode system.
6
+ */
7
+
8
+ /**
9
+ * VPC configuration
10
+ * @typedef {Object} VpcDefinition
11
+ * @property {boolean} enable - Whether VPC is enabled
12
+ *
13
+ * @property {Object} [ownership] - Resource ownership configuration
14
+ * @property {'stack'|'external'|'auto'} [ownership.vpc] - VPC ownership
15
+ * @property {'stack'|'external'|'auto'} [ownership.securityGroup] - Security group ownership
16
+ * @property {'stack'|'external'|'auto'} [ownership.subnets] - Subnets ownership
17
+ * @property {'stack'|'external'|'auto'} [ownership.natGateway] - NAT gateway ownership
18
+ * @property {'stack'|'external'|'auto'} [ownership.vpcEndpoints] - VPC endpoints ownership
19
+ *
20
+ * @property {Object} [external] - External resource references (required if ownership='external')
21
+ * @property {string} [external.vpcId] - External VPC ID
22
+ * @property {string[]} [external.securityGroupIds] - External security group IDs
23
+ * @property {string[]} [external.subnetIds] - External subnet IDs
24
+ * @property {string} [external.natGatewayId] - External NAT gateway ID
25
+ * @property {Object} [external.vpcEndpointIds] - External VPC endpoint IDs
26
+ * @property {string} [external.vpcEndpointIds.s3] - S3 endpoint ID
27
+ * @property {string} [external.vpcEndpointIds.dynamodb] - DynamoDB endpoint ID
28
+ * @property {string} [external.vpcEndpointIds.kms] - KMS endpoint ID
29
+ * @property {string} [external.vpcEndpointIds.secretsManager] - Secrets Manager endpoint ID
30
+ * @property {string} [external.vpcEndpointIds.sqs] - SQS endpoint ID
31
+ *
32
+ * @property {Object} [config] - Configuration preferences
33
+ * @property {boolean} [config.selfHeal] - Auto-configure NAT/routes/etc (default: false)
34
+ * @property {string} [config.cidrBlock] - CIDR block for stack-owned VPC (default: '10.0.0.0/16')
35
+ * @property {boolean} [config.enableVpcEndpoints] - Enable VPC endpoints (default: true)
36
+ * @property {Object} [config.natGateway] - NAT Gateway configuration
37
+ * @property {boolean} [config.natGateway.enable] - Enable NAT Gateway (default: true)
38
+ */
39
+
40
+ /**
41
+ * Aurora PostgreSQL configuration
42
+ * @typedef {Object} AuroraDefinition
43
+ * @property {boolean} enable - Whether Aurora is enabled
44
+ *
45
+ * @property {Object} [ownership] - Resource ownership configuration
46
+ * @property {'stack'|'external'|'auto'} [ownership.cluster] - Aurora cluster ownership
47
+ * @property {'stack'|'external'|'auto'} [ownership.subnetGroup] - DB subnet group ownership
48
+ * @property {'stack'|'external'|'auto'} [ownership.secret] - Secrets Manager secret ownership
49
+ *
50
+ * @property {Object} [external] - External resource references
51
+ * @property {string} [external.clusterId] - External cluster identifier
52
+ * @property {string} [external.clusterEndpoint] - External cluster endpoint
53
+ * @property {number} [external.port] - External cluster port
54
+ * @property {string} [external.secretArn] - External Secrets Manager ARN
55
+ *
56
+ * @property {Object} [config] - Configuration preferences
57
+ * @property {'aurora-postgresql'|'aurora-mysql'} [config.engine] - Database engine
58
+ * @property {number} [config.minCapacity] - Min serverless capacity (default: 0.5)
59
+ * @property {number} [config.maxCapacity] - Max serverless capacity (default: 1)
60
+ * @property {string} [config.database] - Database name (default: 'frigg')
61
+ * @property {boolean} [config.publiclyAccessible] - Public access (default: false)
62
+ * @property {boolean} [config.autoCreateCredentials] - Auto-create credentials in Secrets Manager
63
+ */
64
+
65
+ /**
66
+ * KMS encryption configuration
67
+ * @typedef {Object} KmsDefinition
68
+ * @property {boolean} enable - Whether KMS is enabled
69
+ *
70
+ * @property {Object} [ownership] - Resource ownership configuration
71
+ * @property {'stack'|'external'|'auto'} [ownership.key] - KMS key ownership
72
+ *
73
+ * @property {Object} [external] - External resource references
74
+ * @property {string} [external.keyId] - External KMS key ID or ARN
75
+ * @property {string} [external.keyAlias] - External KMS key alias
76
+ *
77
+ * @property {Object} [config] - Configuration preferences
78
+ * @property {boolean} [config.enableKeyRotation] - Enable automatic key rotation
79
+ * @property {string} [config.description] - Key description
80
+ */
81
+
82
+ /**
83
+ * SSM Parameter Store configuration
84
+ * @typedef {Object} SsmDefinition
85
+ * @property {boolean} enable - Whether SSM is enabled
86
+ * @property {string[]} [parameterPaths] - Parameter paths to grant access to
87
+ */
88
+
89
+ /**
90
+ * Database migration configuration
91
+ * @typedef {Object} MigrationDefinition
92
+ * @property {boolean} enable - Whether migrations are enabled
93
+ * @property {string} [migrationPath] - Path to migration files
94
+ */
95
+
96
+ /**
97
+ * WebSocket configuration
98
+ * @typedef {Object} WebsocketDefinition
99
+ * @property {boolean} enable - Whether WebSocket API is enabled
100
+ */
101
+
102
+ /**
103
+ * Integration configuration
104
+ * @typedef {Object} IntegrationDefinition
105
+ * @property {Object} Definition - Integration definition object
106
+ * @property {string} Definition.name - Integration name
107
+ */
108
+
109
+ /**
110
+ * Complete application definition
111
+ * @typedef {Object} AppDefinition
112
+ * @property {string} name - Application name
113
+ * @property {string} stage - Deployment stage (e.g., 'dev', 'production')
114
+ * @property {string} provider - Cloud provider (e.g., 'aws')
115
+ * @property {string} region - AWS region
116
+ *
117
+ * @property {VpcDefinition} [vpc] - VPC configuration
118
+ * @property {Object} [database] - Database configuration
119
+ * @property {AuroraDefinition} [database.postgres] - PostgreSQL configuration
120
+ * @property {KmsDefinition} [encryption] - KMS encryption configuration
121
+ * @property {SsmDefinition} [ssm] - SSM Parameter Store configuration
122
+ * @property {MigrationDefinition} [migrations] - Database migration configuration
123
+ * @property {WebsocketDefinition} [websockets] - WebSocket API configuration
124
+ * @property {IntegrationDefinition[]} [integrations] - Integration definitions
125
+ *
126
+ * @property {Object} [environment] - Environment variables
127
+ */
128
+
129
+ /**
130
+ * Validate app definition has required fields
131
+ * @param {AppDefinition} appDefinition - App definition to validate
132
+ * @throws {Error} If required fields are missing
133
+ */
134
+ function validateAppDefinition(appDefinition) {
135
+ if (!appDefinition) {
136
+ throw new Error('App definition is required');
137
+ }
138
+
139
+ if (!appDefinition.name) {
140
+ throw new Error('App definition must have a name');
141
+ }
142
+
143
+ if (!appDefinition.provider) {
144
+ throw new Error('App definition must have a provider');
145
+ }
146
+
147
+ if (!appDefinition.region) {
148
+ throw new Error('App definition must have a region');
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get stack name from app definition
154
+ * @param {AppDefinition} appDefinition - App definition
155
+ * @returns {string} Stack name
156
+ */
157
+ function getStackName(appDefinition) {
158
+ const stage = appDefinition.stage || 'dev';
159
+ return `${appDefinition.name}-${stage}`;
160
+ }
161
+
162
+ /**
163
+ * Check if VPC is enabled
164
+ * @param {AppDefinition} appDefinition - App definition
165
+ * @returns {boolean}
166
+ */
167
+ function isVpcEnabled(appDefinition) {
168
+ return appDefinition.vpc?.enable === true;
169
+ }
170
+
171
+ /**
172
+ * Check if Aurora is enabled
173
+ * @param {AppDefinition} appDefinition - App definition
174
+ * @returns {boolean}
175
+ */
176
+ function isAuroraEnabled(appDefinition) {
177
+ return appDefinition.database?.postgres?.enable === true;
178
+ }
179
+
180
+ /**
181
+ * Check if KMS is enabled
182
+ * @param {AppDefinition} appDefinition - App definition
183
+ * @returns {boolean}
184
+ */
185
+ function isKmsEnabled(appDefinition) {
186
+ return appDefinition.encryption?.enable === true;
187
+ }
188
+
189
+ /**
190
+ * Check if SSM is enabled
191
+ * @param {AppDefinition} appDefinition - App definition
192
+ * @returns {boolean}
193
+ */
194
+ function isSsmEnabled(appDefinition) {
195
+ return appDefinition.ssm?.enable === true;
196
+ }
197
+
198
+ module.exports = {
199
+ validateAppDefinition,
200
+ getStackName,
201
+ isVpcEnabled,
202
+ isAuroraEnabled,
203
+ isKmsEnabled,
204
+ isSsmEnabled
205
+ };