@friggframework/devtools 2.0.0--canary.490.10eff0a.0 → 2.0.0--canary.493.f8d621f.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/domains/networking/vpc-builder.js +2 -62
- package/infrastructure/domains/networking/vpc-builder.test.js +0 -140
- package/infrastructure/domains/networking/vpc-resolver.js +10 -57
- package/infrastructure/domains/networking/vpc-resolver.test.js +0 -40
- package/infrastructure/domains/shared/cloudformation-discovery.js +5 -157
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +0 -218
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +0 -23
- package/infrastructure/domains/shared/resource-discovery.js +5 -17
- package/infrastructure/domains/shared/resource-discovery.test.js +0 -36
- package/infrastructure/scripts/build-prisma-layer.js +81 -8
- package/infrastructure/scripts/build-prisma-layer.test.js +53 -1
- package/infrastructure/scripts/verify-prisma-layer.js +72 -0
- package/package.json +7 -7
|
@@ -149,7 +149,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
149
149
|
physicalId = flatDiscovery.defaultVpcId;
|
|
150
150
|
} else if (logicalId === 'FriggLambdaSecurityGroup') {
|
|
151
151
|
resourceType = 'AWS::EC2::SecurityGroup';
|
|
152
|
-
physicalId = flatDiscovery.
|
|
152
|
+
physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
153
153
|
} else if (logicalId === 'FriggPrivateSubnet1') {
|
|
154
154
|
resourceType = 'AWS::EC2::Subnet';
|
|
155
155
|
physicalId = flatDiscovery.privateSubnetId1;
|
|
@@ -159,9 +159,6 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
159
159
|
} else if (logicalId === 'FriggNATGateway') {
|
|
160
160
|
resourceType = 'AWS::EC2::NatGateway';
|
|
161
161
|
physicalId = flatDiscovery.existingNatGatewayId;
|
|
162
|
-
} else if (logicalId === 'FriggLambdaRouteTable') {
|
|
163
|
-
resourceType = 'AWS::EC2::RouteTable';
|
|
164
|
-
physicalId = flatDiscovery.routeTableId;
|
|
165
162
|
} else if (logicalId === 'FriggS3VPCEndpoint') {
|
|
166
163
|
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
167
164
|
physicalId = flatDiscovery.s3VpcEndpointId;
|
|
@@ -187,11 +184,6 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
187
184
|
});
|
|
188
185
|
}
|
|
189
186
|
});
|
|
190
|
-
|
|
191
|
-
// Also check for external resources extracted via CloudFormation queries
|
|
192
|
-
// (e.g., VPC ID from security group query, subnets from route table associations)
|
|
193
|
-
// These are NOT in the stack but were discovered through stack resources
|
|
194
|
-
this._addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds);
|
|
195
187
|
} else {
|
|
196
188
|
// Resources discovered from AWS API (not CloudFormation)
|
|
197
189
|
// These go into external array
|
|
@@ -298,58 +290,6 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
298
290
|
return discovery;
|
|
299
291
|
}
|
|
300
292
|
|
|
301
|
-
/**
|
|
302
|
-
* Add external resources that were discovered via CloudFormation queries
|
|
303
|
-
* (e.g., VPC ID extracted from security group, subnets from route table associations)
|
|
304
|
-
*
|
|
305
|
-
* @private
|
|
306
|
-
*/
|
|
307
|
-
_addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds) {
|
|
308
|
-
// VPC ID extracted from SG or route table (NOT a stack resource)
|
|
309
|
-
if (flatDiscovery.defaultVpcId &&
|
|
310
|
-
typeof flatDiscovery.defaultVpcId === 'string' &&
|
|
311
|
-
!existingLogicalIds.includes('FriggVPC')) {
|
|
312
|
-
discovery.external.push({
|
|
313
|
-
physicalId: flatDiscovery.defaultVpcId,
|
|
314
|
-
resourceType: 'AWS::EC2::VPC',
|
|
315
|
-
source: 'cloudformation-query'
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Subnets extracted from route table associations (NOT stack resources)
|
|
320
|
-
if (flatDiscovery.privateSubnetId1 &&
|
|
321
|
-
typeof flatDiscovery.privateSubnetId1 === 'string' &&
|
|
322
|
-
!existingLogicalIds.includes('FriggPrivateSubnet1')) {
|
|
323
|
-
discovery.external.push({
|
|
324
|
-
physicalId: flatDiscovery.privateSubnetId1,
|
|
325
|
-
resourceType: 'AWS::EC2::Subnet',
|
|
326
|
-
source: 'cloudformation-query'
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (flatDiscovery.privateSubnetId2 &&
|
|
331
|
-
typeof flatDiscovery.privateSubnetId2 === 'string' &&
|
|
332
|
-
!existingLogicalIds.includes('FriggPrivateSubnet2')) {
|
|
333
|
-
discovery.external.push({
|
|
334
|
-
physicalId: flatDiscovery.privateSubnetId2,
|
|
335
|
-
resourceType: 'AWS::EC2::Subnet',
|
|
336
|
-
source: 'cloudformation-query'
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// NAT Gateway extracted from route table routes
|
|
341
|
-
if (flatDiscovery.existingNatGatewayId &&
|
|
342
|
-
typeof flatDiscovery.existingNatGatewayId === 'string' &&
|
|
343
|
-
!existingLogicalIds.includes('FriggNATGateway') &&
|
|
344
|
-
!existingLogicalIds.includes('FriggNatGateway')) {
|
|
345
|
-
discovery.external.push({
|
|
346
|
-
physicalId: flatDiscovery.existingNatGatewayId,
|
|
347
|
-
resourceType: 'AWS::EC2::NatGateway',
|
|
348
|
-
source: 'cloudformation-query'
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
293
|
/**
|
|
354
294
|
* Translate legacy configuration (management modes) to new ownership-based configuration
|
|
355
295
|
* Provides backwards compatibility for existing app definitions
|
|
@@ -1721,7 +1661,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1721
1661
|
// Stack-managed resources should be reused, not recreated
|
|
1722
1662
|
const stackManagedEndpoints = {
|
|
1723
1663
|
s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
|
|
1724
|
-
dynamodb: discoveredResources.
|
|
1664
|
+
dynamodb: discoveredResources.dynamoDbVpcEndpointId && typeof discoveredResources.dynamoDbVpcEndpointId === 'string',
|
|
1725
1665
|
kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
|
|
1726
1666
|
secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
|
|
1727
1667
|
sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
|
|
@@ -1258,145 +1258,5 @@ describe('VpcBuilder', () => {
|
|
|
1258
1258
|
expect(result.outputs.PrivateSubnet2Id).toBeDefined();
|
|
1259
1259
|
});
|
|
1260
1260
|
});
|
|
1261
|
-
|
|
1262
|
-
describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
|
|
1263
|
-
it('should add VPC from CloudFormation query to external array', () => {
|
|
1264
|
-
const flatDiscovery = {
|
|
1265
|
-
fromCloudFormationStack: true,
|
|
1266
|
-
stackName: 'test-stack',
|
|
1267
|
-
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggLambdaSecurityGroup'],
|
|
1268
|
-
// VPC ID was extracted from security group query (NOT a stack resource)
|
|
1269
|
-
defaultVpcId: 'vpc-extracted-from-sg',
|
|
1270
|
-
lambdaSecurityGroupId: 'sg-123',
|
|
1271
|
-
routeTableId: 'rtb-123'
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1275
|
-
|
|
1276
|
-
// VPC should be in external array (discovered via query, not in stack)
|
|
1277
|
-
const vpcExternal = result.external.find(r => r.resourceType === 'AWS::EC2::VPC');
|
|
1278
|
-
expect(vpcExternal).toBeDefined();
|
|
1279
|
-
expect(vpcExternal.physicalId).toBe('vpc-extracted-from-sg');
|
|
1280
|
-
expect(vpcExternal.source).toBe('cloudformation-query');
|
|
1281
|
-
|
|
1282
|
-
// Security group SHOULD be in stackManaged (is in stack)
|
|
1283
|
-
const sgStack = result.stackManaged.find(r => r.logicalId === 'FriggLambdaSecurityGroup');
|
|
1284
|
-
expect(sgStack).toBeDefined();
|
|
1285
|
-
expect(sgStack.physicalId).toBe('sg-123');
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
|
-
it('should add subnets from route table associations to external array', () => {
|
|
1289
|
-
const flatDiscovery = {
|
|
1290
|
-
fromCloudFormationStack: true,
|
|
1291
|
-
stackName: 'test-stack',
|
|
1292
|
-
existingLogicalIds: ['FriggLambdaRouteTable'],
|
|
1293
|
-
routeTableId: 'rtb-123',
|
|
1294
|
-
// Subnets extracted from route table associations (NOT stack resources)
|
|
1295
|
-
privateSubnetId1: 'subnet-1',
|
|
1296
|
-
privateSubnetId2: 'subnet-2'
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1300
|
-
|
|
1301
|
-
// Subnets should be in external array
|
|
1302
|
-
const subnet1 = result.external.find(r => r.physicalId === 'subnet-1');
|
|
1303
|
-
const subnet2 = result.external.find(r => r.physicalId === 'subnet-2');
|
|
1304
|
-
|
|
1305
|
-
expect(subnet1).toBeDefined();
|
|
1306
|
-
expect(subnet1.resourceType).toBe('AWS::EC2::Subnet');
|
|
1307
|
-
expect(subnet1.source).toBe('cloudformation-query');
|
|
1308
|
-
|
|
1309
|
-
expect(subnet2).toBeDefined();
|
|
1310
|
-
expect(subnet2.resourceType).toBe('AWS::EC2::Subnet');
|
|
1311
|
-
expect(subnet2.source).toBe('cloudformation-query');
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1314
|
-
it('should add NAT Gateway from route table queries to external array', () => {
|
|
1315
|
-
const flatDiscovery = {
|
|
1316
|
-
fromCloudFormationStack: true,
|
|
1317
|
-
stackName: 'test-stack',
|
|
1318
|
-
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggPrivateRoute'],
|
|
1319
|
-
routeTableId: 'rtb-123',
|
|
1320
|
-
// NAT Gateway extracted from route table routes (NOT a stack resource)
|
|
1321
|
-
existingNatGatewayId: 'nat-extracted'
|
|
1322
|
-
};
|
|
1323
|
-
|
|
1324
|
-
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1325
|
-
|
|
1326
|
-
// NAT should be in external array
|
|
1327
|
-
const natExternal = result.external.find(r => r.resourceType === 'AWS::EC2::NatGateway');
|
|
1328
|
-
expect(natExternal).toBeDefined();
|
|
1329
|
-
expect(natExternal.physicalId).toBe('nat-extracted');
|
|
1330
|
-
expect(natExternal.source).toBe('cloudformation-query');
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
it('should NOT add resources to external if they are in stack', () => {
|
|
1334
|
-
const flatDiscovery = {
|
|
1335
|
-
fromCloudFormationStack: true,
|
|
1336
|
-
stackName: 'test-stack',
|
|
1337
|
-
existingLogicalIds: ['FriggVPC', 'FriggPrivateSubnet1'],
|
|
1338
|
-
// These ARE in the stack
|
|
1339
|
-
defaultVpcId: 'vpc-in-stack',
|
|
1340
|
-
privateSubnetId1: 'subnet-in-stack'
|
|
1341
|
-
};
|
|
1342
|
-
|
|
1343
|
-
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1344
|
-
|
|
1345
|
-
// Should be in stackManaged, NOT external
|
|
1346
|
-
expect(result.stackManaged.some(r => r.logicalId === 'FriggVPC')).toBe(true);
|
|
1347
|
-
expect(result.stackManaged.some(r => r.logicalId === 'FriggPrivateSubnet1')).toBe(true);
|
|
1348
|
-
|
|
1349
|
-
// Should NOT be in external
|
|
1350
|
-
expect(result.external.some(r => r.physicalId === 'vpc-in-stack')).toBe(false);
|
|
1351
|
-
expect(result.external.some(r => r.physicalId === 'subnet-in-stack')).toBe(false);
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
it('should handle Frontify pattern: stack resources + queried external references', () => {
|
|
1355
|
-
const flatDiscovery = {
|
|
1356
|
-
fromCloudFormationStack: true,
|
|
1357
|
-
stackName: 'create-frigg-app-production',
|
|
1358
|
-
existingLogicalIds: [
|
|
1359
|
-
'FriggLambdaSecurityGroup',
|
|
1360
|
-
'FriggLambdaRouteTable',
|
|
1361
|
-
'FriggPrivateRoute',
|
|
1362
|
-
'FriggPrivateSubnet1RouteTableAssociation',
|
|
1363
|
-
'FriggPrivateSubnet2RouteTableAssociation',
|
|
1364
|
-
'FriggS3VPCEndpoint',
|
|
1365
|
-
'FriggDynamoDBVPCEndpoint',
|
|
1366
|
-
'FriggKMSVPCEndpoint'
|
|
1367
|
-
],
|
|
1368
|
-
// Stack resources
|
|
1369
|
-
lambdaSecurityGroupId: 'sg-01002240c6a446202',
|
|
1370
|
-
routeTableId: 'rtb-08af43bbf0775602d',
|
|
1371
|
-
s3VpcEndpointId: 'vpce-s3',
|
|
1372
|
-
// External resources (discovered via queries)
|
|
1373
|
-
defaultVpcId: 'vpc-0cd17c0e06cb28b28',
|
|
1374
|
-
privateSubnetId1: 'subnet-034f6562dbbc16348',
|
|
1375
|
-
privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
|
|
1376
|
-
existingNatGatewayId: 'nat-022660c36a47e2d79'
|
|
1377
|
-
};
|
|
1378
|
-
|
|
1379
|
-
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1380
|
-
|
|
1381
|
-
// Stack resources should be in stackManaged
|
|
1382
|
-
expect(result.stackManaged).toEqual(
|
|
1383
|
-
expect.arrayContaining([
|
|
1384
|
-
expect.objectContaining({ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-01002240c6a446202' }),
|
|
1385
|
-
expect.objectContaining({ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d' }),
|
|
1386
|
-
expect.objectContaining({ logicalId: 'FriggS3VPCEndpoint' })
|
|
1387
|
-
])
|
|
1388
|
-
);
|
|
1389
|
-
|
|
1390
|
-
// External resources should be in external array
|
|
1391
|
-
expect(result.external).toEqual(
|
|
1392
|
-
expect.arrayContaining([
|
|
1393
|
-
expect.objectContaining({ physicalId: 'vpc-0cd17c0e06cb28b28', resourceType: 'AWS::EC2::VPC', source: 'cloudformation-query' }),
|
|
1394
|
-
expect.objectContaining({ physicalId: 'subnet-034f6562dbbc16348', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1395
|
-
expect.objectContaining({ physicalId: 'subnet-0b8be2b82aeb5cdec', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1396
|
-
expect.objectContaining({ physicalId: 'nat-022660c36a47e2d79', resourceType: 'AWS::EC2::NatGateway', source: 'cloudformation-query' })
|
|
1397
|
-
])
|
|
1398
|
-
);
|
|
1399
|
-
});
|
|
1400
|
-
});
|
|
1401
1261
|
});
|
|
1402
1262
|
|
|
@@ -23,7 +23,6 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
23
23
|
*/
|
|
24
24
|
resolveVpc(appDefinition, discovery) {
|
|
25
25
|
const userIntent = appDefinition.vpc?.ownership?.vpc || 'auto';
|
|
26
|
-
const vpcManagement = appDefinition.vpc?.management; // Legacy config
|
|
27
26
|
|
|
28
27
|
// Explicit external
|
|
29
28
|
if (userIntent === 'external') {
|
|
@@ -44,35 +43,20 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
// Auto-decide
|
|
47
|
-
|
|
46
|
+
return this.resolveResourceOwnership(
|
|
48
47
|
'auto',
|
|
49
48
|
'FriggVPC',
|
|
50
49
|
'AWS::EC2::VPC',
|
|
51
50
|
discovery
|
|
52
51
|
);
|
|
53
|
-
|
|
54
|
-
// CRITICAL: If auto-resolution wants to create a VPC but management mode is 'discover' (or undefined),
|
|
55
|
-
// throw an error instead. Creating a VPC is expensive and should be explicit.
|
|
56
|
-
if (decision.ownership === ResourceOwnership.STACK &&
|
|
57
|
-
!decision.physicalId &&
|
|
58
|
-
vpcManagement !== 'create-new' &&
|
|
59
|
-
userIntent === 'auto') {
|
|
60
|
-
throw new Error(
|
|
61
|
-
'VPC discovery failed: No VPC found. ' +
|
|
62
|
-
'Either set vpc.management to "create-new" or provide vpc.vpcId with vpc.management "use-existing".'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return decision;
|
|
67
52
|
}
|
|
68
53
|
|
|
69
54
|
/**
|
|
70
55
|
* Resolve Security Group ownership
|
|
71
56
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* - Otherwise → STACK (create FriggLambdaSecurityGroup)
|
|
57
|
+
* Special logic: We ALWAYS create our own FriggLambdaSecurityGroup with specific
|
|
58
|
+
* rules unless the user explicitly provides external SG IDs. The discovered
|
|
59
|
+
* defaultSecurityGroupId is the VPC's default SG, but we need our own Lambda SG.
|
|
76
60
|
*
|
|
77
61
|
* @param {Object} appDefinition - App definition
|
|
78
62
|
* @param {Object} discovery - Discovery result
|
|
@@ -81,7 +65,7 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
81
65
|
resolveSecurityGroup(appDefinition, discovery) {
|
|
82
66
|
const userIntent = appDefinition.vpc?.ownership?.securityGroup || 'auto';
|
|
83
67
|
|
|
84
|
-
// Explicit external - use
|
|
68
|
+
// Explicit external - only use external SGs if user explicitly provides them
|
|
85
69
|
if (userIntent === 'external') {
|
|
86
70
|
this.requireExternalIds(
|
|
87
71
|
appDefinition.vpc?.external?.securityGroupIds,
|
|
@@ -93,52 +77,21 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
93
77
|
);
|
|
94
78
|
}
|
|
95
79
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
99
|
-
return this.createStackDecision(
|
|
100
|
-
inStack?.physicalId,
|
|
101
|
-
inStack
|
|
102
|
-
? 'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
103
|
-
: 'User specified ownership=stack - will create FriggLambdaSecurityGroup'
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Auto mode: Check stack first, then check for discovered default SG
|
|
80
|
+
// For stack or auto: check if FriggLambdaSecurityGroup exists in stack
|
|
81
|
+
// If it does, reuse it. If not, create it. Never use discovered default SG.
|
|
108
82
|
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
109
83
|
|
|
110
84
|
if (inStack) {
|
|
111
85
|
return this.createStackDecision(
|
|
112
86
|
inStack.physicalId,
|
|
113
|
-
'Found FriggLambdaSecurityGroup in CloudFormation stack
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Also check flat discovery for lambdaSecurityGroupId (from CloudFormation extraction)
|
|
118
|
-
const structured = discovery._structured || discovery;
|
|
119
|
-
const lambdaSgId = structured.lambdaSecurityGroupId || discovery.lambdaSecurityGroupId;
|
|
120
|
-
|
|
121
|
-
if (lambdaSgId) {
|
|
122
|
-
return this.createStackDecision(
|
|
123
|
-
lambdaSgId,
|
|
124
|
-
'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check for discovered default security group (from external VPC pattern)
|
|
129
|
-
const defaultSgId = structured.defaultSecurityGroupId || discovery.defaultSecurityGroupId;
|
|
130
|
-
|
|
131
|
-
if (defaultSgId) {
|
|
132
|
-
return this.createExternalDecision(
|
|
133
|
-
[defaultSgId],
|
|
134
|
-
'Found default security group via discovery - will reuse (matches canary behavior)'
|
|
87
|
+
'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
135
88
|
);
|
|
136
89
|
}
|
|
137
90
|
|
|
138
|
-
//
|
|
91
|
+
// Create new FriggLambdaSecurityGroup in stack
|
|
139
92
|
return this.createStackDecision(
|
|
140
93
|
null,
|
|
141
|
-
'No
|
|
94
|
+
'No existing FriggLambdaSecurityGroup - will create in stack'
|
|
142
95
|
);
|
|
143
96
|
}
|
|
144
97
|
|
|
@@ -109,46 +109,6 @@ describe('VpcResourceResolver', () => {
|
|
|
109
109
|
expect(decision.physicalId).toBeUndefined();
|
|
110
110
|
expect(decision.reason).toContain('No existing resource found');
|
|
111
111
|
});
|
|
112
|
-
|
|
113
|
-
it('should throw error when auto mode finds no VPC and management is not create-new', () => {
|
|
114
|
-
const appDefinition = {
|
|
115
|
-
vpc: {
|
|
116
|
-
enable: true,
|
|
117
|
-
// No management specified - defaults to discover
|
|
118
|
-
// No ownership specified - defaults to auto
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
const discovery = {
|
|
122
|
-
stackManaged: [],
|
|
123
|
-
external: [],
|
|
124
|
-
fromCloudFormation: false
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Should throw error instead of trying to create VPC
|
|
128
|
-
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
129
|
-
'VPC discovery failed: No VPC found'
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should allow creating VPC when management is create-new', () => {
|
|
134
|
-
const appDefinition = {
|
|
135
|
-
vpc: {
|
|
136
|
-
enable: true,
|
|
137
|
-
management: 'create-new'
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
const discovery = {
|
|
141
|
-
stackManaged: [],
|
|
142
|
-
external: [],
|
|
143
|
-
fromCloudFormation: false
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Should NOT throw - create-new explicitly allows VPC creation
|
|
147
|
-
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
148
|
-
|
|
149
|
-
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
150
|
-
expect(decision.physicalId).toBeNull();
|
|
151
|
-
});
|
|
152
112
|
});
|
|
153
113
|
|
|
154
114
|
describe('resolveSecurityGroup', () => {
|
|
@@ -28,9 +28,6 @@ class CloudFormationDiscovery {
|
|
|
28
28
|
*/
|
|
29
29
|
async discoverFromStack(stackName) {
|
|
30
30
|
try {
|
|
31
|
-
// Store stack name for use in helper methods
|
|
32
|
-
this.currentStackName = stackName;
|
|
33
|
-
|
|
34
31
|
// Try to get the stack
|
|
35
32
|
const stack = await this.provider.describeStack(stackName);
|
|
36
33
|
|
|
@@ -114,93 +111,6 @@ class CloudFormationDiscovery {
|
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
/**
|
|
118
|
-
* Extract external resource references from stack resource properties
|
|
119
|
-
*
|
|
120
|
-
* When VPC/subnets/NAT are external, they're referenced in routing resources' properties.
|
|
121
|
-
* We query EC2 to get the actual VPC ID, NAT Gateway ID, and subnet IDs from the route table.
|
|
122
|
-
*
|
|
123
|
-
* @private
|
|
124
|
-
* @param {Array} resources - CloudFormation stack resources
|
|
125
|
-
* @param {Object} discovered - Object to populate with discovered resources
|
|
126
|
-
*/
|
|
127
|
-
async _extractExternalReferencesFromStackResources(resources, discovered) {
|
|
128
|
-
if (!this.provider || !this.provider.getEC2Client) {
|
|
129
|
-
console.log(' ℹ Skipping external reference extraction (EC2 client not available)');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
// If we found a route table in the stack, query EC2 for its details
|
|
135
|
-
// This gives us VPC ID, NAT Gateway ID, and subnet IDs
|
|
136
|
-
if (discovered.routeTableId) {
|
|
137
|
-
try {
|
|
138
|
-
console.log(` ℹ Querying route table ${discovered.routeTableId} for external references...`);
|
|
139
|
-
const { DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
|
|
140
|
-
const ec2 = this.provider.getEC2Client();
|
|
141
|
-
const rtResponse = await ec2.send(new DescribeRouteTablesCommand({
|
|
142
|
-
RouteTableIds: [discovered.routeTableId]
|
|
143
|
-
}));
|
|
144
|
-
|
|
145
|
-
if (rtResponse.RouteTables && rtResponse.RouteTables.length > 0) {
|
|
146
|
-
const routeTable = rtResponse.RouteTables[0];
|
|
147
|
-
|
|
148
|
-
// Extract VPC ID
|
|
149
|
-
if (routeTable.VpcId && !discovered.defaultVpcId) {
|
|
150
|
-
discovered.defaultVpcId = routeTable.VpcId;
|
|
151
|
-
console.log(` ✓ Extracted VPC ID from route table: ${routeTable.VpcId}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Extract NAT Gateway ID from routes
|
|
155
|
-
const natRoute = routeTable.Routes?.find(r => r.NatGatewayId);
|
|
156
|
-
if (natRoute && natRoute.NatGatewayId && !discovered.natGatewayId) {
|
|
157
|
-
discovered.natGatewayId = natRoute.NatGatewayId;
|
|
158
|
-
discovered.existingNatGatewayId = natRoute.NatGatewayId;
|
|
159
|
-
console.log(` ✓ Extracted NAT Gateway ID from routes: ${natRoute.NatGatewayId}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Extract subnet IDs from route table associations
|
|
163
|
-
const associations = routeTable.Associations || [];
|
|
164
|
-
const subnetAssociations = associations.filter(a => a.SubnetId);
|
|
165
|
-
|
|
166
|
-
if (subnetAssociations.length >= 1 && !discovered.privateSubnetId1) {
|
|
167
|
-
discovered.privateSubnetId1 = subnetAssociations[0].SubnetId;
|
|
168
|
-
console.log(` ✓ Extracted private subnet 1 from associations: ${subnetAssociations[0].SubnetId}`);
|
|
169
|
-
}
|
|
170
|
-
if (subnetAssociations.length >= 2 && !discovered.privateSubnetId2) {
|
|
171
|
-
discovered.privateSubnetId2 = subnetAssociations[1].SubnetId;
|
|
172
|
-
console.log(` ✓ Extracted private subnet 2 from associations: ${subnetAssociations[1].SubnetId}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Query for default security group in the VPC (matches canary behavior)
|
|
176
|
-
if (routeTable.VpcId && !discovered.defaultSecurityGroupId) {
|
|
177
|
-
try {
|
|
178
|
-
const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
|
|
179
|
-
const sgResponse = await ec2.send(new DescribeSecurityGroupsCommand({
|
|
180
|
-
Filters: [
|
|
181
|
-
{ Name: 'vpc-id', Values: [routeTable.VpcId] },
|
|
182
|
-
{ Name: 'group-name', Values: ['default'] }
|
|
183
|
-
]
|
|
184
|
-
}));
|
|
185
|
-
|
|
186
|
-
if (sgResponse.SecurityGroups && sgResponse.SecurityGroups.length > 0) {
|
|
187
|
-
discovered.defaultSecurityGroupId = sgResponse.SecurityGroups[0].GroupId;
|
|
188
|
-
console.log(` ✓ Extracted default security group: ${discovered.defaultSecurityGroupId}`);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.warn(` ⚠️ Could not query default security group: ${error.message}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.warn(` ⚠️ Could not query route table for external references: ${error.message}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
} catch (error) {
|
|
200
|
-
console.warn(` ⚠️ Error extracting external references: ${error.message}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
114
|
/**
|
|
205
115
|
* Extract discovered resources from CloudFormation stack resources
|
|
206
116
|
*
|
|
@@ -306,30 +216,6 @@ class CloudFormationDiscovery {
|
|
|
306
216
|
discovered.natGatewayId = PhysicalResourceId;
|
|
307
217
|
}
|
|
308
218
|
|
|
309
|
-
// Route Table (Lambda route table for external VPC pattern)
|
|
310
|
-
if (LogicalResourceId === 'FriggLambdaRouteTable' && ResourceType === 'AWS::EC2::RouteTable') {
|
|
311
|
-
discovered.routeTableId = PhysicalResourceId;
|
|
312
|
-
discovered.privateRouteTableId = PhysicalResourceId;
|
|
313
|
-
console.log(` ✓ Found route table in stack: ${PhysicalResourceId}`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// NAT Route (proves NAT configuration exists) - support both naming patterns
|
|
317
|
-
if ((LogicalResourceId === 'FriggNATRoute' || LogicalResourceId === 'FriggPrivateRoute') &&
|
|
318
|
-
ResourceType === 'AWS::EC2::Route') {
|
|
319
|
-
discovered.natRoute = PhysicalResourceId;
|
|
320
|
-
console.log(` ✓ Found NAT route in stack: ${LogicalResourceId}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Route Table Associations (links subnets to route table)
|
|
324
|
-
if (LogicalResourceId.includes('RouteAssociation') &&
|
|
325
|
-
ResourceType === 'AWS::EC2::SubnetRouteTableAssociation') {
|
|
326
|
-
if (!discovered.routeTableAssociations) {
|
|
327
|
-
discovered.routeTableAssociations = [];
|
|
328
|
-
}
|
|
329
|
-
discovered.routeTableAssociations.push(PhysicalResourceId);
|
|
330
|
-
console.log(` ✓ Found route table association: ${LogicalResourceId}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
219
|
// VPC - direct extraction (primary method)
|
|
334
220
|
if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
|
|
335
221
|
discovered.defaultVpcId = PhysicalResourceId;
|
|
@@ -390,64 +276,26 @@ class CloudFormationDiscovery {
|
|
|
390
276
|
// VPC Endpoint Security Group
|
|
391
277
|
if (LogicalResourceId === 'FriggVPCEndpointSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
|
|
392
278
|
discovered.vpcEndpointSecurityGroupId = PhysicalResourceId;
|
|
393
|
-
console.log(` ✓ Found VPC endpoint security group in stack: ${PhysicalResourceId}`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Lambda Security Group (if created in stack)
|
|
397
|
-
if (LogicalResourceId === 'FriggLambdaSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
|
|
398
|
-
discovered.lambdaSecurityGroupId = PhysicalResourceId;
|
|
399
|
-
// Also set as defaultSecurityGroupId so converter recognizes it
|
|
400
|
-
discovered.defaultSecurityGroupId = PhysicalResourceId;
|
|
401
|
-
console.log(` ✓ Found Lambda security group in stack: ${PhysicalResourceId}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// VPC Endpoints - support both old and new naming conventions
|
|
405
|
-
// Initialize vpcEndpoints object for structured access
|
|
406
|
-
if (!discovered.vpcEndpoints) {
|
|
407
|
-
discovered.vpcEndpoints = {};
|
|
408
279
|
}
|
|
409
280
|
|
|
410
|
-
//
|
|
411
|
-
if (
|
|
412
|
-
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
281
|
+
// VPC Endpoints
|
|
282
|
+
if (LogicalResourceId === 'FriggS3VPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
413
283
|
discovered.s3VpcEndpointId = PhysicalResourceId;
|
|
414
|
-
discovered.vpcEndpoints.s3 = PhysicalResourceId;
|
|
415
|
-
console.log(` ✓ Found S3 VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
416
284
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if ((LogicalResourceId === 'FriggDynamoDBVPCEndpoint' || LogicalResourceId === 'VPCEndpointDynamoDB') &&
|
|
420
|
-
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
421
|
-
discovered.dynamodbVpcEndpointId = PhysicalResourceId; // Note: all lowercase for consistency
|
|
422
|
-
discovered.vpcEndpoints.dynamodb = PhysicalResourceId;
|
|
423
|
-
console.log(` ✓ Found DynamoDB VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
285
|
+
if (LogicalResourceId === 'FriggDynamoDBVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
286
|
+
discovered.dynamoDbVpcEndpointId = PhysicalResourceId;
|
|
424
287
|
}
|
|
425
|
-
|
|
426
|
-
// KMS Endpoint (both naming patterns)
|
|
427
|
-
if ((LogicalResourceId === 'FriggKMSVPCEndpoint' || LogicalResourceId === 'VPCEndpointKMS') &&
|
|
428
|
-
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
288
|
+
if (LogicalResourceId === 'FriggKMSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
429
289
|
discovered.kmsVpcEndpointId = PhysicalResourceId;
|
|
430
|
-
discovered.vpcEndpoints.kms = PhysicalResourceId;
|
|
431
|
-
console.log(` ✓ Found KMS VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
432
290
|
}
|
|
433
|
-
|
|
434
|
-
// Secrets Manager Endpoint
|
|
435
291
|
if (LogicalResourceId === 'FriggSecretsManagerVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
436
292
|
discovered.secretsManagerVpcEndpointId = PhysicalResourceId;
|
|
437
|
-
discovered.vpcEndpoints.secretsManager = PhysicalResourceId;
|
|
438
293
|
}
|
|
439
|
-
|
|
440
|
-
// SQS Endpoint
|
|
441
294
|
if (LogicalResourceId === 'FriggSQSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
442
295
|
discovered.sqsVpcEndpointId = PhysicalResourceId;
|
|
443
|
-
discovered.vpcEndpoints.sqs = PhysicalResourceId;
|
|
444
296
|
}
|
|
445
297
|
}
|
|
446
298
|
|
|
447
|
-
// Extract VPC ID and other external references from routing resource properties
|
|
448
|
-
// This handles the pattern where VPC is external but routing is in the stack
|
|
449
|
-
await this._extractExternalReferencesFromStackResources(resources, discovered);
|
|
450
|
-
|
|
451
299
|
// If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
|
|
452
300
|
if (discovered.defaultVpcId && this.provider &&
|
|
453
301
|
!discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
|
|
@@ -586,223 +586,5 @@ describe('CloudFormationDiscovery', () => {
|
|
|
586
586
|
expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
|
|
587
587
|
});
|
|
588
588
|
});
|
|
589
|
-
|
|
590
|
-
describe('External VPC with routing infrastructure pattern', () => {
|
|
591
|
-
it('should discover routing resources when VPC is external', async () => {
|
|
592
|
-
// This tests the Frontify pattern: external VPC/subnets/KMS,
|
|
593
|
-
// but stack creates routing infrastructure (route table, NAT route, VPC endpoints)
|
|
594
|
-
const mockStack = {
|
|
595
|
-
StackName: 'create-frigg-app-production',
|
|
596
|
-
Outputs: [],
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const mockResources = [
|
|
600
|
-
{
|
|
601
|
-
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
602
|
-
PhysicalResourceId: 'rtb-0b83aca77ccde20a6',
|
|
603
|
-
ResourceType: 'AWS::EC2::RouteTable',
|
|
604
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
LogicalResourceId: 'FriggNATRoute',
|
|
608
|
-
PhysicalResourceId: 'rtb-0b83aca77ccde20a6|0.0.0.0/0',
|
|
609
|
-
ResourceType: 'AWS::EC2::Route',
|
|
610
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
LogicalResourceId: 'FriggSubnet1RouteAssociation',
|
|
614
|
-
PhysicalResourceId: 'rtbassoc-07245da0b447ca469',
|
|
615
|
-
ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
616
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
LogicalResourceId: 'FriggSubnet2RouteAssociation',
|
|
620
|
-
PhysicalResourceId: 'rtbassoc-0806f9783c4ea181f',
|
|
621
|
-
ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
622
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
LogicalResourceId: 'VPCEndpointS3',
|
|
626
|
-
PhysicalResourceId: 'vpce-0352ceac2124c14be',
|
|
627
|
-
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
628
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
LogicalResourceId: 'VPCEndpointDynamoDB',
|
|
632
|
-
PhysicalResourceId: 'vpce-0b06c4f631199ea68',
|
|
633
|
-
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
634
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
635
|
-
},
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
639
|
-
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
640
|
-
|
|
641
|
-
const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
|
|
642
|
-
|
|
643
|
-
// Verify routing infrastructure was discovered
|
|
644
|
-
expect(result.routeTableId).toBe('rtb-0b83aca77ccde20a6');
|
|
645
|
-
expect(result.privateRouteTableId).toBe('rtb-0b83aca77ccde20a6');
|
|
646
|
-
expect(result.natRoute).toBe('rtb-0b83aca77ccde20a6|0.0.0.0/0');
|
|
647
|
-
expect(result.routeTableAssociations).toEqual([
|
|
648
|
-
'rtbassoc-07245da0b447ca469',
|
|
649
|
-
'rtbassoc-0806f9783c4ea181f',
|
|
650
|
-
]);
|
|
651
|
-
|
|
652
|
-
// Verify VPC endpoints were discovered (both naming conventions)
|
|
653
|
-
expect(result.vpcEndpoints).toBeDefined();
|
|
654
|
-
expect(result.vpcEndpoints.s3).toBe('vpce-0352ceac2124c14be');
|
|
655
|
-
expect(result.vpcEndpoints.dynamodb).toBe('vpce-0b06c4f631199ea68');
|
|
656
|
-
expect(result.s3VpcEndpointId).toBe('vpce-0352ceac2124c14be');
|
|
657
|
-
expect(result.dynamoDbVpcEndpointId).toBe('vpce-0b06c4f631199ea68');
|
|
658
|
-
|
|
659
|
-
// Verify NO VPC/KMS resources (they're external)
|
|
660
|
-
expect(result.defaultVpcId).toBeUndefined();
|
|
661
|
-
expect(result.defaultKmsKeyId).toBeUndefined();
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('should work with legacy VPC endpoint naming (FriggS3VPCEndpoint)', async () => {
|
|
665
|
-
const mockStack = {
|
|
666
|
-
StackName: 'test-stack',
|
|
667
|
-
Outputs: [],
|
|
668
|
-
};
|
|
669
|
-
|
|
670
|
-
const mockResources = [
|
|
671
|
-
{
|
|
672
|
-
LogicalResourceId: 'FriggS3VPCEndpoint',
|
|
673
|
-
PhysicalResourceId: 'vpce-legacy-s3',
|
|
674
|
-
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
675
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
LogicalResourceId: 'FriggDynamoDBVPCEndpoint',
|
|
679
|
-
PhysicalResourceId: 'vpce-legacy-ddb',
|
|
680
|
-
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
681
|
-
ResourceStatus: 'CREATE_COMPLETE',
|
|
682
|
-
},
|
|
683
|
-
];
|
|
684
|
-
|
|
685
|
-
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
686
|
-
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
687
|
-
|
|
688
|
-
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
689
|
-
|
|
690
|
-
// Both naming conventions should work
|
|
691
|
-
expect(result.vpcEndpoints.s3).toBe('vpce-legacy-s3');
|
|
692
|
-
expect(result.vpcEndpoints.dynamodb).toBe('vpce-legacy-ddb');
|
|
693
|
-
expect(result.s3VpcEndpointId).toBe('vpce-legacy-s3');
|
|
694
|
-
expect(result.dynamoDbVpcEndpointId).toBe('vpce-legacy-ddb');
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
it('should extract FriggLambdaSecurityGroup from stack', async () => {
|
|
698
|
-
const mockStack = {
|
|
699
|
-
StackName: 'test-stack',
|
|
700
|
-
Outputs: [],
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
const mockResources = [
|
|
704
|
-
{
|
|
705
|
-
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
706
|
-
PhysicalResourceId: 'sg-01002240c6a446202',
|
|
707
|
-
ResourceType: 'AWS::EC2::SecurityGroup',
|
|
708
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
709
|
-
},
|
|
710
|
-
{
|
|
711
|
-
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
712
|
-
PhysicalResourceId: 'rtb-08af43bbf0775602d',
|
|
713
|
-
ResourceType: 'AWS::EC2::RouteTable',
|
|
714
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
715
|
-
},
|
|
716
|
-
];
|
|
717
|
-
|
|
718
|
-
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
719
|
-
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
720
|
-
|
|
721
|
-
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
722
|
-
|
|
723
|
-
// Lambda security group should be extracted
|
|
724
|
-
expect(result.lambdaSecurityGroupId).toBe('sg-01002240c6a446202');
|
|
725
|
-
expect(result.defaultSecurityGroupId).toBe('sg-01002240c6a446202');
|
|
726
|
-
expect(result.existingLogicalIds).toContain('FriggLambdaSecurityGroup');
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
it('should support FriggPrivateRoute naming for NAT routes', async () => {
|
|
730
|
-
const mockStack = {
|
|
731
|
-
StackName: 'test-stack',
|
|
732
|
-
Outputs: [],
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
const mockResources = [
|
|
736
|
-
{
|
|
737
|
-
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
738
|
-
PhysicalResourceId: 'rtb-123',
|
|
739
|
-
ResourceType: 'AWS::EC2::RouteTable',
|
|
740
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
LogicalResourceId: 'FriggPrivateRoute',
|
|
744
|
-
PhysicalResourceId: 'rtb-123|0.0.0.0/0',
|
|
745
|
-
ResourceType: 'AWS::EC2::Route',
|
|
746
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
747
|
-
},
|
|
748
|
-
];
|
|
749
|
-
|
|
750
|
-
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
751
|
-
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
752
|
-
|
|
753
|
-
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
754
|
-
|
|
755
|
-
// Both FriggNATRoute and FriggPrivateRoute should be recognized
|
|
756
|
-
expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
|
|
757
|
-
expect(result.routeTableId).toBe('rtb-123');
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
it('should extract external references from route table without stackName error', async () => {
|
|
761
|
-
const mockStack = {
|
|
762
|
-
StackName: 'test-stack',
|
|
763
|
-
Outputs: [],
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
const mockResources = [
|
|
767
|
-
{
|
|
768
|
-
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
769
|
-
PhysicalResourceId: 'rtb-real-id',
|
|
770
|
-
ResourceType: 'AWS::EC2::RouteTable',
|
|
771
|
-
ResourceStatus: 'UPDATE_COMPLETE',
|
|
772
|
-
},
|
|
773
|
-
];
|
|
774
|
-
|
|
775
|
-
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
776
|
-
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
777
|
-
|
|
778
|
-
// Mock EC2 DescribeRouteTables to return route table with VPC info
|
|
779
|
-
mockProvider.getEC2Client = jest.fn().mockReturnValue({
|
|
780
|
-
send: jest.fn().mockResolvedValue({
|
|
781
|
-
RouteTables: [{
|
|
782
|
-
RouteTableId: 'rtb-real-id',
|
|
783
|
-
VpcId: 'vpc-extracted',
|
|
784
|
-
Routes: [
|
|
785
|
-
{ NatGatewayId: 'nat-extracted', DestinationCidrBlock: '0.0.0.0/0' }
|
|
786
|
-
],
|
|
787
|
-
Associations: [
|
|
788
|
-
{ SubnetId: 'subnet-1' },
|
|
789
|
-
{ SubnetId: 'subnet-2' }
|
|
790
|
-
]
|
|
791
|
-
}]
|
|
792
|
-
})
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
796
|
-
|
|
797
|
-
// Should extract VPC, NAT, and subnets from route table
|
|
798
|
-
expect(result.defaultVpcId).toBe('vpc-extracted');
|
|
799
|
-
expect(result.existingNatGatewayId).toBe('nat-extracted');
|
|
800
|
-
expect(result.privateSubnetId1).toBe('subnet-1');
|
|
801
|
-
expect(result.privateSubnetId2).toBe('subnet-2');
|
|
802
|
-
|
|
803
|
-
// Should NOT throw 'stackName is not defined' error
|
|
804
|
-
expect(result).toBeDefined();
|
|
805
|
-
});
|
|
806
|
-
});
|
|
807
589
|
});
|
|
808
590
|
|
|
@@ -536,29 +536,6 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
536
536
|
return [];
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Describe a specific stack resource to get its full details including properties
|
|
542
|
-
* @param {string} stackName - Stack name
|
|
543
|
-
* @param {string} logicalResourceId - Logical resource ID
|
|
544
|
-
* @returns {Promise<Object>} Resource details
|
|
545
|
-
*/
|
|
546
|
-
async describeStackResource(stackName, logicalResourceId) {
|
|
547
|
-
const cf = this.getCloudFormationClient();
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
const { DescribeStackResourceCommand } = require('@aws-sdk/client-cloudformation');
|
|
551
|
-
const response = await cf.send(new DescribeStackResourceCommand({
|
|
552
|
-
StackName: stackName,
|
|
553
|
-
LogicalResourceId: logicalResourceId,
|
|
554
|
-
}));
|
|
555
|
-
|
|
556
|
-
return response.StackResourceDetail || null;
|
|
557
|
-
} catch (error) {
|
|
558
|
-
console.warn(`Failed to describe stack resource ${logicalResourceId}:`, error.message);
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
539
|
}
|
|
563
540
|
|
|
564
541
|
module.exports = {
|
|
@@ -95,23 +95,11 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
95
95
|
const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
|
|
96
96
|
const stackResources = await cfDiscovery.discoverFromStack(stackName);
|
|
97
97
|
|
|
98
|
-
// Validate CF discovery results -
|
|
99
|
-
const hasVpcData = stackResources?.defaultVpcId;
|
|
100
|
-
const hasKmsData = stackResources?.defaultKmsKeyId;
|
|
101
|
-
const hasAuroraData = stackResources?.auroraClusterId;
|
|
102
|
-
|
|
103
|
-
// Check for routing infrastructure (proves VPC config exists even with external VPC)
|
|
104
|
-
const hasRoutingInfra = stackResources?.routeTableId || // FriggLambdaRouteTable
|
|
105
|
-
stackResources?.natRoute || // FriggNATRoute
|
|
106
|
-
stackResources?.vpcEndpoints?.s3 || // VPC endpoints
|
|
107
|
-
stackResources?.vpcEndpoints?.dynamodb;
|
|
108
|
-
|
|
109
|
-
// Stack is useful if it has EITHER actual resources OR routing infrastructure
|
|
110
|
-
const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData || hasRoutingInfra;
|
|
111
|
-
|
|
112
|
-
if (hasRoutingInfra && !hasVpcData) {
|
|
113
|
-
console.log(' ✓ Found VPC routing infrastructure in stack (external VPC pattern)');
|
|
114
|
-
}
|
|
98
|
+
// Validate CF discovery results - only use if contains useful data
|
|
99
|
+
const hasVpcData = stackResources?.defaultVpcId;
|
|
100
|
+
const hasKmsData = stackResources?.defaultKmsKeyId;
|
|
101
|
+
const hasAuroraData = stackResources?.auroraClusterId;
|
|
102
|
+
const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData;
|
|
115
103
|
|
|
116
104
|
// Check if we're in isolated mode (each stage gets its own VPC/Aurora)
|
|
117
105
|
const isIsolatedMode = appDefinition.managementMode === 'managed' &&
|
|
@@ -415,42 +415,6 @@ describe('Resource Discovery', () => {
|
|
|
415
415
|
delete process.env.SLS_STAGE;
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
-
it('should recognize routing infrastructure as useful data', async () => {
|
|
419
|
-
const appDefinition = {
|
|
420
|
-
name: 'test-app',
|
|
421
|
-
vpc: { enable: true },
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
process.env.SLS_STAGE = 'production';
|
|
425
|
-
|
|
426
|
-
// Mock CloudFormation discovery to return routing infrastructure but no VPC resource
|
|
427
|
-
const mockCloudFormationDiscovery = {
|
|
428
|
-
discoverFromStack: jest.fn().mockResolvedValue({
|
|
429
|
-
fromCloudFormationStack: true,
|
|
430
|
-
routeTableId: 'rtb-123',
|
|
431
|
-
natRoute: 'rtb-123|0.0.0.0/0',
|
|
432
|
-
vpcEndpoints: {
|
|
433
|
-
s3: 'vpce-s3',
|
|
434
|
-
dynamodb: 'vpce-ddb'
|
|
435
|
-
},
|
|
436
|
-
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggNATRoute']
|
|
437
|
-
// NO defaultVpcId, NO defaultKmsKeyId, NO auroraClusterId
|
|
438
|
-
})
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
442
|
-
CloudFormationDiscovery.mockImplementation(() => mockCloudFormationDiscovery);
|
|
443
|
-
|
|
444
|
-
const result = await gatherDiscoveredResources(appDefinition);
|
|
445
|
-
|
|
446
|
-
// Should use CloudFormation data without falling back to AWS API
|
|
447
|
-
expect(result.routeTableId).toBe('rtb-123');
|
|
448
|
-
expect(result.vpcEndpoints.s3).toBe('vpce-s3');
|
|
449
|
-
|
|
450
|
-
// Should NOT call AWS API discovery
|
|
451
|
-
expect(mockVpcDiscovery.discover).not.toHaveBeenCalled();
|
|
452
|
-
});
|
|
453
|
-
|
|
454
418
|
it('should include secrets in SSM discovery by default', async () => {
|
|
455
419
|
const appDefinition = {
|
|
456
420
|
ssm: { enable: true },
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* The CLI is only needed for migrations and is packaged separately with the dbMigrate function.
|
|
11
11
|
*
|
|
12
12
|
* The layer is configured based on AppDefinition database settings:
|
|
13
|
-
* - PostgreSQL: Includes PostgreSQL client + query engine
|
|
14
|
-
* - MongoDB: Includes MongoDB client + query engine
|
|
13
|
+
* - PostgreSQL: Includes PostgreSQL client + query engine + migrations
|
|
14
|
+
* - MongoDB: Includes MongoDB client + query engine + migrations (if needed)
|
|
15
15
|
* - Defaults to PostgreSQL only if not specified
|
|
16
16
|
*
|
|
17
17
|
* Usage:
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
* layers/prisma/nodejs/node_modules/
|
|
23
23
|
* ├── @prisma/client (runtime only, ~10-15MB)
|
|
24
24
|
* ├── generated/prisma-postgresql (if PostgreSQL enabled)
|
|
25
|
+
* │ ├── schema.prisma
|
|
26
|
+
* │ └── migrations/
|
|
25
27
|
* └── generated/prisma-mongodb (if MongoDB enabled)
|
|
28
|
+
* ├── schema.prisma
|
|
29
|
+
* └── migrations/
|
|
26
30
|
*
|
|
27
31
|
* See: LAMBDA-LAYER-PRISMA.md for complete documentation
|
|
28
32
|
*/
|
|
@@ -85,6 +89,27 @@ function getGeneratedClientPackages(databaseConfig = {}) {
|
|
|
85
89
|
return packages;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
function getMigrationsPackages(clientPackages) {
|
|
93
|
+
return clientPackages.map(pkg => ({
|
|
94
|
+
dbType: pkg.replace('generated/prisma-', ''),
|
|
95
|
+
clientPackage: pkg
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getMigrationSourcePath(searchPaths, dbType) {
|
|
100
|
+
for (const searchPath of searchPaths) {
|
|
101
|
+
const candidatePath = path.join(searchPath, `prisma-${dbType}`, 'migrations');
|
|
102
|
+
if (fs.existsSync(candidatePath)) {
|
|
103
|
+
return candidatePath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getMigrationDestinationPath(layerNodeModules, clientPackage) {
|
|
110
|
+
return path.join(layerNodeModules, clientPackage, 'migrations');
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
// Configuration
|
|
89
114
|
// Script runs from integration project root (e.g., backend/)
|
|
90
115
|
// and reads Prisma packages from @friggframework/core
|
|
@@ -299,11 +324,47 @@ async function copyPrismaPackages(clientPackages) {
|
|
|
299
324
|
logSuccess(`Copied ${copiedCount} generated client packages from @friggframework/core`);
|
|
300
325
|
}
|
|
301
326
|
|
|
327
|
+
async function copyMigrations(clientPackages) {
|
|
328
|
+
logStep(5, 'Copying migrations from @friggframework/core');
|
|
329
|
+
|
|
330
|
+
const workspaceNodeModules = path.join(path.dirname(CORE_PACKAGE_PATH), '..');
|
|
331
|
+
const searchPaths = [
|
|
332
|
+
path.join(CORE_PACKAGE_PATH, 'node_modules'),
|
|
333
|
+
path.join(PROJECT_ROOT, 'node_modules'),
|
|
334
|
+
workspaceNodeModules,
|
|
335
|
+
CORE_PACKAGE_PATH,
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const migrations = getMigrationsPackages(clientPackages);
|
|
339
|
+
let copiedCount = 0;
|
|
340
|
+
|
|
341
|
+
for (const { dbType, clientPackage } of migrations) {
|
|
342
|
+
const sourcePath = getMigrationSourcePath(searchPaths, dbType);
|
|
343
|
+
|
|
344
|
+
if (sourcePath) {
|
|
345
|
+
const destPath = getMigrationDestinationPath(LAYER_NODE_MODULES, clientPackage);
|
|
346
|
+
await fs.copy(sourcePath, destPath, { dereference: true });
|
|
347
|
+
|
|
348
|
+
const fromLocation = sourcePath.includes('@friggframework/core/prisma')
|
|
349
|
+
? 'core package'
|
|
350
|
+
: 'workspace';
|
|
351
|
+
logSuccess(`Copied migrations for ${dbType} (from ${fromLocation})`);
|
|
352
|
+
copiedCount++;
|
|
353
|
+
} else {
|
|
354
|
+
logWarning(`Migrations not found for ${dbType} - this may cause migration failures`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (copiedCount > 0) {
|
|
359
|
+
logSuccess(`Copied migrations for ${copiedCount} database ${copiedCount === 1 ? 'type' : 'types'}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
302
363
|
/**
|
|
303
364
|
* Remove unnecessary files to reduce layer size
|
|
304
365
|
*/
|
|
305
366
|
async function removeUnnecessaryFiles() {
|
|
306
|
-
logStep(
|
|
367
|
+
logStep(6, 'Removing unnecessary files (source maps, docs, tests)');
|
|
307
368
|
|
|
308
369
|
let removedCount = 0;
|
|
309
370
|
let totalSize = 0;
|
|
@@ -341,7 +402,7 @@ async function removeUnnecessaryFiles() {
|
|
|
341
402
|
* Remove non-rhel engine binaries to reduce layer size
|
|
342
403
|
*/
|
|
343
404
|
async function removeNonRhelBinaries() {
|
|
344
|
-
logStep(
|
|
405
|
+
logStep(7, 'Removing non-rhel engine binaries');
|
|
345
406
|
|
|
346
407
|
let removedCount = 0;
|
|
347
408
|
let totalSize = 0;
|
|
@@ -380,7 +441,7 @@ async function removeNonRhelBinaries() {
|
|
|
380
441
|
* @param {Array} expectedClients - List of client packages that should have binaries
|
|
381
442
|
*/
|
|
382
443
|
async function verifyRhelBinaries(expectedClients) {
|
|
383
|
-
logStep(
|
|
444
|
+
logStep(8, 'Verifying rhel-openssl-3.0.x binaries are present');
|
|
384
445
|
|
|
385
446
|
try {
|
|
386
447
|
const findCmd = `find "${LAYER_NODE_MODULES}" -name "*rhel-openssl-3.0.x*" 2>/dev/null || true`;
|
|
@@ -414,7 +475,7 @@ async function verifyRhelBinaries(expectedClients) {
|
|
|
414
475
|
* @param {Array} clientPackages - Generated client packages that were included
|
|
415
476
|
*/
|
|
416
477
|
async function verifyLayerStructure(clientPackages) {
|
|
417
|
-
logStep(
|
|
478
|
+
logStep(9, 'Verifying layer structure (runtime only)');
|
|
418
479
|
|
|
419
480
|
const requiredPaths = [
|
|
420
481
|
'@prisma/client/runtime',
|
|
@@ -426,6 +487,11 @@ async function verifyLayerStructure(clientPackages) {
|
|
|
426
487
|
requiredPaths.push(`${pkg}/schema.prisma`);
|
|
427
488
|
}
|
|
428
489
|
|
|
490
|
+
// Add migrations directory for each included client
|
|
491
|
+
for (const pkg of clientPackages) {
|
|
492
|
+
requiredPaths.push(`${pkg}/migrations/migration_lock.toml`);
|
|
493
|
+
}
|
|
494
|
+
|
|
429
495
|
// Verify CLI is NOT present (keeps layer small)
|
|
430
496
|
const forbiddenPaths = [
|
|
431
497
|
'prisma/build',
|
|
@@ -463,7 +529,7 @@ async function verifyLayerStructure(clientPackages) {
|
|
|
463
529
|
* Calculate and display final layer size
|
|
464
530
|
*/
|
|
465
531
|
async function displayLayerSummary() {
|
|
466
|
-
logStep(
|
|
532
|
+
logStep(10, 'Layer build summary');
|
|
467
533
|
|
|
468
534
|
const layerSizeMB = getDirectorySize(LAYER_OUTPUT_PATH);
|
|
469
535
|
|
|
@@ -514,6 +580,7 @@ async function buildPrismaLayer(databaseConfig = {}) {
|
|
|
514
580
|
await createLayerStructure();
|
|
515
581
|
await installPrismaPackages(); // Install runtime client only (NO CLI)
|
|
516
582
|
await copyPrismaPackages(clientPackages); // Copy generated clients from core
|
|
583
|
+
await copyMigrations(clientPackages); // Copy migrations from core
|
|
517
584
|
await removeUnnecessaryFiles(); // Remove source maps, docs, tests (37MB+)
|
|
518
585
|
await removeNonRhelBinaries(); // Remove non-Linux binaries
|
|
519
586
|
await verifyRhelBinaries(clientPackages); // Verify query engines present
|
|
@@ -550,4 +617,10 @@ if (require.main === module) {
|
|
|
550
617
|
.catch(() => process.exit(1));
|
|
551
618
|
}
|
|
552
619
|
|
|
553
|
-
module.exports = {
|
|
620
|
+
module.exports = {
|
|
621
|
+
buildPrismaLayer,
|
|
622
|
+
getGeneratedClientPackages,
|
|
623
|
+
getMigrationsPackages,
|
|
624
|
+
getMigrationSourcePath,
|
|
625
|
+
getMigrationDestinationPath
|
|
626
|
+
};
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* Validates database client selection logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
getGeneratedClientPackages,
|
|
8
|
+
getMigrationsPackages,
|
|
9
|
+
getMigrationSourcePath,
|
|
10
|
+
getMigrationDestinationPath
|
|
11
|
+
} = require('./build-prisma-layer');
|
|
7
12
|
|
|
8
13
|
// Mock the log function
|
|
9
14
|
jest.mock('./build-prisma-layer', () => {
|
|
@@ -11,6 +16,9 @@ jest.mock('./build-prisma-layer', () => {
|
|
|
11
16
|
return {
|
|
12
17
|
...actual,
|
|
13
18
|
getGeneratedClientPackages: actual.getGeneratedClientPackages,
|
|
19
|
+
getMigrationsPackages: actual.getMigrationsPackages,
|
|
20
|
+
getMigrationSourcePath: actual.getMigrationSourcePath,
|
|
21
|
+
getMigrationDestinationPath: actual.getMigrationDestinationPath,
|
|
14
22
|
};
|
|
15
23
|
});
|
|
16
24
|
|
|
@@ -99,4 +107,48 @@ describe('getGeneratedClientPackages()', () => {
|
|
|
99
107
|
});
|
|
100
108
|
});
|
|
101
109
|
|
|
110
|
+
describe('getMigrationsPackages()', () => {
|
|
111
|
+
it('should extract database types from client packages', () => {
|
|
112
|
+
const clientPackages = ['generated/prisma-postgresql', 'generated/prisma-mongodb'];
|
|
113
|
+
const migrations = getMigrationsPackages(clientPackages);
|
|
102
114
|
|
|
115
|
+
expect(migrations).toEqual([
|
|
116
|
+
{ dbType: 'postgresql', clientPackage: 'generated/prisma-postgresql' },
|
|
117
|
+
{ dbType: 'mongodb', clientPackage: 'generated/prisma-mongodb' }
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle single client package', () => {
|
|
122
|
+
const clientPackages = ['generated/prisma-postgresql'];
|
|
123
|
+
const migrations = getMigrationsPackages(clientPackages);
|
|
124
|
+
|
|
125
|
+
expect(migrations).toEqual([
|
|
126
|
+
{ dbType: 'postgresql', clientPackage: 'generated/prisma-postgresql' }
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle empty array', () => {
|
|
131
|
+
const migrations = getMigrationsPackages([]);
|
|
132
|
+
expect(migrations).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('getMigrationSourcePath()', () => {
|
|
137
|
+
it('should return correct source path for database type', () => {
|
|
138
|
+
const searchPaths = ['/workspace/packages/core'];
|
|
139
|
+
const dbType = 'postgresql';
|
|
140
|
+
|
|
141
|
+
const sourcePath = getMigrationSourcePath(searchPaths, dbType);
|
|
142
|
+
expect(sourcePath).toBe('/workspace/packages/core/prisma-postgresql/migrations');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('getMigrationDestinationPath()', () => {
|
|
147
|
+
it('should return correct destination path for client package', () => {
|
|
148
|
+
const layerNodeModules = '/layers/prisma/nodejs/node_modules';
|
|
149
|
+
const clientPackage = 'generated/prisma-postgresql';
|
|
150
|
+
|
|
151
|
+
const destPath = getMigrationDestinationPath(layerNodeModules, clientPackage);
|
|
152
|
+
expect(destPath).toBe('/layers/prisma/nodejs/node_modules/generated/prisma-postgresql/migrations');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = process.cwd();
|
|
7
|
+
const LAYER_PATH = path.join(PROJECT_ROOT, 'layers/prisma/nodejs/node_modules');
|
|
8
|
+
|
|
9
|
+
async function verifyLayerStructure() {
|
|
10
|
+
console.log('Verifying Prisma layer structure...\n');
|
|
11
|
+
|
|
12
|
+
const checks = [
|
|
13
|
+
{
|
|
14
|
+
name: 'PostgreSQL schema',
|
|
15
|
+
path: 'generated/prisma-postgresql/schema.prisma'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'PostgreSQL migrations directory',
|
|
19
|
+
path: 'generated/prisma-postgresql/migrations'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'PostgreSQL migration_lock.toml',
|
|
23
|
+
path: 'generated/prisma-postgresql/migrations/migration_lock.toml'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: '@prisma/client runtime',
|
|
27
|
+
path: '@prisma/client/runtime'
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
let allPassed = true;
|
|
32
|
+
|
|
33
|
+
for (const check of checks) {
|
|
34
|
+
const fullPath = path.join(LAYER_PATH, check.path);
|
|
35
|
+
const exists = await fs.pathExists(fullPath);
|
|
36
|
+
|
|
37
|
+
if (exists) {
|
|
38
|
+
console.log(`✓ ${check.name}`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`✗ ${check.name} (missing)`);
|
|
41
|
+
allPassed = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\n');
|
|
46
|
+
|
|
47
|
+
if (allPassed) {
|
|
48
|
+
console.log('✓ All checks passed!');
|
|
49
|
+
const migrationsPath = path.join(LAYER_PATH, 'generated/prisma-postgresql/migrations');
|
|
50
|
+
const migrationFiles = await fs.readdir(migrationsPath);
|
|
51
|
+
console.log(`\nFound ${migrationFiles.length} items in migrations directory:`);
|
|
52
|
+
migrationFiles.forEach(file => {
|
|
53
|
+
console.log(` - ${file}`);
|
|
54
|
+
});
|
|
55
|
+
return 0;
|
|
56
|
+
} else {
|
|
57
|
+
console.log('✗ Some checks failed!');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (require.main === module) {
|
|
63
|
+
verifyLayerStructure()
|
|
64
|
+
.then(code => process.exit(code))
|
|
65
|
+
.catch(err => {
|
|
66
|
+
console.error('Error:', err.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { verifyLayerStructure };
|
|
72
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.493.f8d621f.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"@babel/eslint-parser": "^7.18.9",
|
|
17
17
|
"@babel/parser": "^7.25.3",
|
|
18
18
|
"@babel/traverse": "^7.25.3",
|
|
19
|
-
"@friggframework/core": "2.0.0--canary.
|
|
20
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
21
|
-
"@friggframework/test": "2.0.0--canary.
|
|
19
|
+
"@friggframework/core": "2.0.0--canary.493.f8d621f.0",
|
|
20
|
+
"@friggframework/schemas": "2.0.0--canary.493.f8d621f.0",
|
|
21
|
+
"@friggframework/test": "2.0.0--canary.493.f8d621f.0",
|
|
22
22
|
"@hapi/boom": "^10.0.1",
|
|
23
23
|
"@inquirer/prompts": "^5.3.8",
|
|
24
24
|
"axios": "^1.7.2",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"validate-npm-package-name": "^5.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
49
|
+
"@friggframework/eslint-config": "2.0.0--canary.493.f8d621f.0",
|
|
50
|
+
"@friggframework/prettier-config": "2.0.0--canary.493.f8d621f.0",
|
|
51
51
|
"aws-sdk-client-mock": "^4.1.0",
|
|
52
52
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
53
53
|
"jest": "^30.1.3",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "f8d621f5fdc06756c1351e99a4686767a46f7064"
|
|
83
83
|
}
|