@friggframework/devtools 2.0.0--canary.490.3374b8f.0 → 2.0.0--canary.490.1f4f770.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/README.md +28 -0
- package/infrastructure/domains/database/migration-builder.js +19 -13
- package/infrastructure/domains/database/migration-builder.test.js +57 -0
- package/infrastructure/domains/integration/integration-builder.js +19 -11
- package/infrastructure/domains/networking/vpc-builder.js +18 -7
- package/infrastructure/domains/networking/vpc-builder.test.js +70 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +27 -17
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +73 -0
- package/infrastructure/infrastructure-composer.js +11 -3
- package/package.json +7 -7
package/infrastructure/README.md
CHANGED
|
@@ -257,6 +257,34 @@ aws lambda get-function-configuration \
|
|
|
257
257
|
--query 'Layers[*].Arn'
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
**Disabling Prisma Layer (Bundle with Functions):**
|
|
261
|
+
|
|
262
|
+
By default, Frigg uses a Lambda Layer for Prisma. You can disable this and bundle Prisma directly with each function:
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const appDefinition = {
|
|
266
|
+
name: 'my-app',
|
|
267
|
+
usePrismaLambdaLayer: false, // Bundle Prisma with each function
|
|
268
|
+
integrations: [{ Definition: { name: 'asana' } }],
|
|
269
|
+
};
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**When to disable the Prisma Layer:**
|
|
273
|
+
|
|
274
|
+
- ✅ CI/CD IAM user lacks `lambda:PublishLayerVersion` permission
|
|
275
|
+
- ✅ Deploying to environments with Lambda layer restrictions
|
|
276
|
+
- ✅ Prefer simpler deployment without layer management
|
|
277
|
+
- ✅ Debugging Prisma client loading issues
|
|
278
|
+
|
|
279
|
+
**Trade-offs:**
|
|
280
|
+
|
|
281
|
+
| Mode | Function Size | Deploy Speed | IAM Permissions Required |
|
|
282
|
+
|------|--------------|--------------|-------------------------|
|
|
283
|
+
| **Layer (default)** | ~45MB per function | Faster (layer cached) | `lambda:PublishLayerVersion` |
|
|
284
|
+
| **Bundled** | ~80MB per function | Slower (Prisma uploaded 5x) | None (layer-related) |
|
|
285
|
+
|
|
286
|
+
**Note:** When `usePrismaLambdaLayer: false`, Prisma client automatically detects the bundled location at runtime. No additional configuration needed.
|
|
287
|
+
|
|
260
288
|
## Usage Examples
|
|
261
289
|
|
|
262
290
|
### Basic Deployment
|
|
@@ -57,6 +57,9 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
57
57
|
// Backwards compatibility: Translate old schema to new ownership schema
|
|
58
58
|
appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
|
|
59
59
|
|
|
60
|
+
// Determine if using Prisma Lambda Layer
|
|
61
|
+
const usePrismaLayer = appDefinition.usePrismaLambdaLayer !== false;
|
|
62
|
+
|
|
60
63
|
const result = {
|
|
61
64
|
functions: {}, // Lambda function definitions
|
|
62
65
|
resources: {},
|
|
@@ -76,7 +79,7 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
76
79
|
console.log(` Queue: ${decisions.queue.ownership} - ${decisions.queue.reason}`);
|
|
77
80
|
|
|
78
81
|
// Build resources based on ownership decisions
|
|
79
|
-
await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
|
|
82
|
+
await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result, usePrismaLayer);
|
|
80
83
|
|
|
81
84
|
console.log(`[${this.name}] ✅ Migration infrastructure configuration completed`);
|
|
82
85
|
return result;
|
|
@@ -207,7 +210,7 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
207
210
|
/**
|
|
208
211
|
* Build migration resources based on ownership decisions
|
|
209
212
|
*/
|
|
210
|
-
async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
|
|
213
|
+
async buildFromDecisions(decisions, appDefinition, discoveredResources, result, usePrismaLayer = true) {
|
|
211
214
|
// Determine if we need to create resources or use existing ones
|
|
212
215
|
const shouldCreateBucket = decisions.bucket.ownership === ResourceOwnership.STACK;
|
|
213
216
|
const shouldCreateQueue = decisions.queue.ownership === ResourceOwnership.STACK;
|
|
@@ -215,12 +218,12 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
215
218
|
if (shouldCreateBucket && shouldCreateQueue && !decisions.bucket.physicalId && !decisions.queue.physicalId) {
|
|
216
219
|
// Create all new migration infrastructure
|
|
217
220
|
console.log(' → Creating new migration infrastructure in stack');
|
|
218
|
-
await this.createMigrationInfrastructure(appDefinition, result);
|
|
221
|
+
await this.createMigrationInfrastructure(appDefinition, result, usePrismaLayer);
|
|
219
222
|
} else if ((decisions.bucket.ownership === ResourceOwnership.STACK && decisions.bucket.physicalId) ||
|
|
220
223
|
(decisions.queue.ownership === ResourceOwnership.STACK && decisions.queue.physicalId)) {
|
|
221
224
|
// Resources exist in stack - add definitions (CloudFormation idempotency)
|
|
222
225
|
console.log(' → Adding migration definitions to template (existing in stack)');
|
|
223
|
-
await this.createMigrationInfrastructure(appDefinition, result);
|
|
226
|
+
await this.createMigrationInfrastructure(appDefinition, result, usePrismaLayer);
|
|
224
227
|
} else {
|
|
225
228
|
// Use external resources
|
|
226
229
|
console.log(' → Using external migration resources');
|
|
@@ -232,18 +235,21 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
232
235
|
* Create Lambda function definitions for database migrations
|
|
233
236
|
* Based on refactor/add-better-support-for-commands branch implementation
|
|
234
237
|
*/
|
|
235
|
-
async createFunctionDefinitions(result) {
|
|
238
|
+
async createFunctionDefinitions(result, usePrismaLayer = true) {
|
|
236
239
|
console.log(' 🔍 DEBUG: createFunctionDefinitions called');
|
|
237
240
|
console.log(' 🔍 DEBUG: result.functions is:', typeof result.functions, result.functions);
|
|
238
241
|
// Migration WORKER package config (needs Prisma CLI WASM files)
|
|
239
242
|
const migrationWorkerPackageConfig = {
|
|
240
243
|
individually: true,
|
|
241
244
|
exclude: [
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Conditionally exclude Prisma (only if using layer)
|
|
246
|
+
...(usePrismaLayer ? [
|
|
247
|
+
// Exclude Prisma runtime client - it's in the Lambda Layer
|
|
248
|
+
'node_modules/@prisma/client/**',
|
|
249
|
+
'node_modules/.prisma/**',
|
|
250
|
+
'node_modules/@friggframework/core/generated/**',
|
|
251
|
+
// But KEEP node_modules/prisma/** (the CLI with WASM)
|
|
252
|
+
] : []),
|
|
247
253
|
|
|
248
254
|
// Exclude ALL nested node_modules
|
|
249
255
|
'node_modules/**/node_modules/**',
|
|
@@ -422,7 +428,7 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
422
428
|
console.log(' 🔍 DEBUG: About to create dbMigrationWorker...');
|
|
423
429
|
result.functions.dbMigrationWorker = {
|
|
424
430
|
handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
|
|
425
|
-
layers: [{ Ref: 'PrismaLambdaLayer' }], //
|
|
431
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // Conditionally use layer
|
|
426
432
|
skipEsbuild: true,
|
|
427
433
|
timeout: 900, // 15 minutes for long migrations
|
|
428
434
|
memorySize: 1024, // Extra memory for Prisma operations
|
|
@@ -484,12 +490,12 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
484
490
|
* Create migration infrastructure CloudFormation resources
|
|
485
491
|
* Creates S3 bucket, SQS queue, and Lambda function definitions
|
|
486
492
|
*/
|
|
487
|
-
async createMigrationInfrastructure(appDefinition, result) {
|
|
493
|
+
async createMigrationInfrastructure(appDefinition, result, usePrismaLayer = true) {
|
|
488
494
|
console.log(' 🔍 DEBUG: createMigrationInfrastructure called');
|
|
489
495
|
console.log(' 🔍 DEBUG: result object before createFunctionDefinitions:', Object.keys(result));
|
|
490
496
|
|
|
491
497
|
// Create Lambda function definitions first (they reference the queue)
|
|
492
|
-
await this.createFunctionDefinitions(result);
|
|
498
|
+
await this.createFunctionDefinitions(result, usePrismaLayer);
|
|
493
499
|
|
|
494
500
|
console.log(' 🔍 DEBUG: result.functions after createFunctionDefinitions:', Object.keys(result.functions || {}));
|
|
495
501
|
|
|
@@ -290,5 +290,62 @@ describe('MigrationBuilder', () => {
|
|
|
290
290
|
expect(builder.getName()).toBe('MigrationBuilder');
|
|
291
291
|
});
|
|
292
292
|
});
|
|
293
|
+
|
|
294
|
+
describe('usePrismaLayer configuration', () => {
|
|
295
|
+
it('should include Prisma layer in migration worker when usePrismaLayer=true (default)', async () => {
|
|
296
|
+
const appDef = {
|
|
297
|
+
database: {
|
|
298
|
+
postgres: { enable: true },
|
|
299
|
+
},
|
|
300
|
+
usePrismaLambdaLayer: true,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = await builder.build(appDef, {});
|
|
304
|
+
|
|
305
|
+
expect(result.functions.dbMigrationWorker.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
306
|
+
|
|
307
|
+
// Prisma client should be excluded from package (but not CLI)
|
|
308
|
+
expect(result.functions.dbMigrationWorker.package.exclude).toEqual(
|
|
309
|
+
expect.arrayContaining([
|
|
310
|
+
'node_modules/@prisma/client/**',
|
|
311
|
+
'node_modules/.prisma/**',
|
|
312
|
+
'node_modules/@friggframework/core/generated/**',
|
|
313
|
+
])
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should NOT include Prisma layer when usePrismaLayer=false', async () => {
|
|
318
|
+
const appDef = {
|
|
319
|
+
database: {
|
|
320
|
+
postgres: { enable: true },
|
|
321
|
+
},
|
|
322
|
+
usePrismaLambdaLayer: false,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const result = await builder.build(appDef, {});
|
|
326
|
+
|
|
327
|
+
expect(result.functions.dbMigrationWorker.layers).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should bundle Prisma CLI with migration worker when usePrismaLayer=false', async () => {
|
|
331
|
+
const appDef = {
|
|
332
|
+
database: {
|
|
333
|
+
postgres: { enable: true },
|
|
334
|
+
},
|
|
335
|
+
usePrismaLambdaLayer: false,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const result = await builder.build(appDef, {});
|
|
339
|
+
|
|
340
|
+
// Prisma should NOT be excluded from package (will be bundled)
|
|
341
|
+
expect(result.functions.dbMigrationWorker.package.exclude).not.toEqual(
|
|
342
|
+
expect.arrayContaining([
|
|
343
|
+
'node_modules/@prisma/client/**',
|
|
344
|
+
'node_modules/.prisma/**',
|
|
345
|
+
'node_modules/@friggframework/core/generated/**',
|
|
346
|
+
])
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
293
350
|
});
|
|
294
351
|
|
|
@@ -62,6 +62,9 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
62
62
|
console.log(`\n[${this.name}] Configuring integrations...`);
|
|
63
63
|
console.log(` Processing ${appDefinition.integrations.length} integrations...`);
|
|
64
64
|
|
|
65
|
+
// Determine if using Prisma Lambda Layer
|
|
66
|
+
const usePrismaLayer = appDefinition.usePrismaLambdaLayer !== false;
|
|
67
|
+
|
|
65
68
|
const result = {
|
|
66
69
|
functions: {},
|
|
67
70
|
resources: {},
|
|
@@ -87,7 +90,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
// Build resources based on ownership decisions
|
|
90
|
-
await this.buildFromDecisions(decisions, appDefinition, result);
|
|
93
|
+
await this.buildFromDecisions(decisions, appDefinition, result, usePrismaLayer);
|
|
91
94
|
|
|
92
95
|
console.log(`[${this.name}] ✅ Integration configuration completed`);
|
|
93
96
|
return result;
|
|
@@ -142,7 +145,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
142
145
|
/**
|
|
143
146
|
* Build integration resources based on ownership decisions
|
|
144
147
|
*/
|
|
145
|
-
async buildFromDecisions(decisions, appDefinition, result) {
|
|
148
|
+
async buildFromDecisions(decisions, appDefinition, result, usePrismaLayer = true) {
|
|
146
149
|
// Create InternalErrorQueue if ownership = STACK
|
|
147
150
|
const shouldCreateInternalErrorQueue = decisions.internalErrorQueue.ownership === ResourceOwnership.STACK;
|
|
148
151
|
|
|
@@ -155,7 +158,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
// Create Lambda function definitions and queue resources for each integration
|
|
158
|
-
const functionPackageConfig = this.createFunctionPackageConfig();
|
|
161
|
+
const functionPackageConfig = this.createFunctionPackageConfig(usePrismaLayer);
|
|
159
162
|
|
|
160
163
|
for (const integration of appDefinition.integrations) {
|
|
161
164
|
const integrationName = integration.Definition.name;
|
|
@@ -164,7 +167,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
164
167
|
console.log(`\n Adding integration: ${integrationName}`);
|
|
165
168
|
|
|
166
169
|
// Create Lambda function definitions (serverless template code)
|
|
167
|
-
await this.createFunctionDefinitions(integration, functionPackageConfig, result);
|
|
170
|
+
await this.createFunctionDefinitions(integration, functionPackageConfig, result, usePrismaLayer);
|
|
168
171
|
|
|
169
172
|
// Create or reference SQS queue based on ownership decision
|
|
170
173
|
const shouldCreateQueue = queueDecision.ownership === ResourceOwnership.STACK;
|
|
@@ -182,18 +185,20 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
182
185
|
/**
|
|
183
186
|
* Create function package exclusion configuration
|
|
184
187
|
*/
|
|
185
|
-
createFunctionPackageConfig() {
|
|
188
|
+
createFunctionPackageConfig(usePrismaLayer = true) {
|
|
186
189
|
return {
|
|
187
190
|
exclude: [
|
|
188
191
|
// Exclude AWS SDK (provided by Lambda runtime)
|
|
189
192
|
'node_modules/aws-sdk/**',
|
|
190
193
|
'node_modules/@aws-sdk/**',
|
|
191
194
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
// Conditionally exclude Prisma (only if using Lambda Layer)
|
|
196
|
+
...(usePrismaLayer ? [
|
|
197
|
+
'node_modules/@prisma/**',
|
|
198
|
+
'node_modules/.prisma/**',
|
|
199
|
+
'node_modules/prisma/**',
|
|
200
|
+
'node_modules/@friggframework/core/generated/**',
|
|
201
|
+
] : []),
|
|
197
202
|
|
|
198
203
|
// Exclude ALL nested node_modules
|
|
199
204
|
'node_modules/**/node_modules/**',
|
|
@@ -249,7 +254,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
249
254
|
* Create Lambda function definitions for an integration
|
|
250
255
|
* These are serverless framework template function definitions
|
|
251
256
|
*/
|
|
252
|
-
async createFunctionDefinitions(integration, functionPackageConfig, result) {
|
|
257
|
+
async createFunctionDefinitions(integration, functionPackageConfig, result, usePrismaLayer = true) {
|
|
253
258
|
const integrationName = integration.Definition.name;
|
|
254
259
|
|
|
255
260
|
// Add webhook handler if enabled (BEFORE catch-all proxy route)
|
|
@@ -261,6 +266,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
261
266
|
|
|
262
267
|
result.functions[webhookFunctionName] = {
|
|
263
268
|
handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
|
|
269
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
264
270
|
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
265
271
|
package: functionPackageConfig,
|
|
266
272
|
events: [
|
|
@@ -284,6 +290,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
284
290
|
// Create HTTP API handler for integration (catch-all route AFTER webhooks)
|
|
285
291
|
result.functions[integrationName] = {
|
|
286
292
|
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
293
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
287
294
|
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
288
295
|
package: functionPackageConfig,
|
|
289
296
|
events: [
|
|
@@ -301,6 +308,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
301
308
|
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
302
309
|
result.functions[queueWorkerName] = {
|
|
303
310
|
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
311
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
304
312
|
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
305
313
|
package: functionPackageConfig,
|
|
306
314
|
reservedConcurrency: 5,
|
|
@@ -552,7 +552,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
552
552
|
this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
|
|
553
553
|
|
|
554
554
|
// Build VPC Endpoints based on ownership decisions
|
|
555
|
-
this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, appDefinition, result);
|
|
555
|
+
this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, result);
|
|
556
556
|
|
|
557
557
|
// Set VPC_ENABLED environment variable
|
|
558
558
|
result.environment.VPC_ENABLED = 'true';
|
|
@@ -920,7 +920,8 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
920
920
|
/**
|
|
921
921
|
* Build VPC Endpoints based on ownership decisions
|
|
922
922
|
*/
|
|
923
|
-
buildVpcEndpointsFromDecisions(
|
|
923
|
+
buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, result) {
|
|
924
|
+
const decisions = endpointDecisions; // For backwards compatibility with existing code
|
|
924
925
|
const endpointsToCreate = [];
|
|
925
926
|
const endpointsInStack = [];
|
|
926
927
|
const externalEndpoints = [];
|
|
@@ -939,7 +940,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
939
940
|
if (endpointsInStack.length > 0) {
|
|
940
941
|
console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
|
|
941
942
|
// CRITICAL: Must add stack-managed endpoints back to template or CloudFormation will DELETE them!
|
|
942
|
-
this._addStackManagedEndpointsToTemplate(decisions, result);
|
|
943
|
+
this._addStackManagedEndpointsToTemplate(decisions, securityGroupDecision, result);
|
|
943
944
|
}
|
|
944
945
|
|
|
945
946
|
if (externalEndpoints.length > 0) {
|
|
@@ -1002,6 +1003,15 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1002
1003
|
// Create security group for interface endpoints if needed
|
|
1003
1004
|
const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
|
|
1004
1005
|
if (needsInterfaceEndpoints) {
|
|
1006
|
+
// Determine source security group for ingress rule
|
|
1007
|
+
let sourceSgId;
|
|
1008
|
+
if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
|
|
1009
|
+
sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
|
|
1010
|
+
} else {
|
|
1011
|
+
// External - use the physical ID
|
|
1012
|
+
sourceSgId = securityGroupDecision.physicalIds[0];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1005
1015
|
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
1006
1016
|
Type: 'AWS::EC2::SecurityGroup',
|
|
1007
1017
|
Properties: {
|
|
@@ -1012,7 +1022,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1012
1022
|
IpProtocol: 'tcp',
|
|
1013
1023
|
FromPort: 443,
|
|
1014
1024
|
ToPort: 443,
|
|
1015
|
-
SourceSecurityGroupId:
|
|
1025
|
+
SourceSecurityGroupId: sourceSgId,
|
|
1016
1026
|
Description: 'HTTPS from Lambda',
|
|
1017
1027
|
},
|
|
1018
1028
|
],
|
|
@@ -1129,7 +1139,8 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1129
1139
|
*
|
|
1130
1140
|
* @private
|
|
1131
1141
|
*/
|
|
1132
|
-
_addStackManagedEndpointsToTemplate(
|
|
1142
|
+
_addStackManagedEndpointsToTemplate(endpointDecisions, securityGroupDecision, result) {
|
|
1143
|
+
const decisions = endpointDecisions; // For backwards compatibility
|
|
1133
1144
|
const vpcId = result.vpcId;
|
|
1134
1145
|
const logicalIdMap = {
|
|
1135
1146
|
s3: 'FriggS3VPCEndpoint',
|
|
@@ -1197,11 +1208,11 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1197
1208
|
// If Lambda SG is stack-managed, use CloudFormation Ref
|
|
1198
1209
|
// If Lambda SG is external, use the physical ID directly
|
|
1199
1210
|
let sourceSgId;
|
|
1200
|
-
if (
|
|
1211
|
+
if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
|
|
1201
1212
|
sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
|
|
1202
1213
|
} else {
|
|
1203
1214
|
// External - use the physical ID
|
|
1204
|
-
sourceSgId =
|
|
1215
|
+
sourceSgId = securityGroupDecision.physicalIds[0];
|
|
1205
1216
|
}
|
|
1206
1217
|
|
|
1207
1218
|
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
@@ -1472,6 +1472,76 @@ describe('VpcBuilder', () => {
|
|
|
1472
1472
|
});
|
|
1473
1473
|
});
|
|
1474
1474
|
|
|
1475
|
+
describe('VPC Endpoint Security Group with External Lambda SG', () => {
|
|
1476
|
+
it('should use external Lambda SG ID (not Ref) for VPC endpoint SG when Lambda SG is external', async () => {
|
|
1477
|
+
const appDefinition = {
|
|
1478
|
+
vpc: {
|
|
1479
|
+
enable: true,
|
|
1480
|
+
enableVPCEndpoints: true,
|
|
1481
|
+
ownership: {
|
|
1482
|
+
securityGroup: 'external' // External Lambda SG
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
1486
|
+
};
|
|
1487
|
+
const discoveredResources = {
|
|
1488
|
+
fromCloudFormationStack: true,
|
|
1489
|
+
defaultVpcId: 'vpc-123',
|
|
1490
|
+
defaultSecurityGroupId: 'sg-default-456', // Default VPC SG
|
|
1491
|
+
lambdaSecurityGroupId: 'sg-stack-789', // Stack-managed SG (will be ignored)
|
|
1492
|
+
privateSubnetId1: 'subnet-1',
|
|
1493
|
+
privateSubnetId2: 'subnet-2',
|
|
1494
|
+
natGatewayId: 'nat-123',
|
|
1495
|
+
existingLogicalIds: ['FriggS3VPCEndpoint', 'FriggKMSVPCEndpoint'],
|
|
1496
|
+
s3VpcEndpointId: 'vpce-s3-stack',
|
|
1497
|
+
kmsVpcEndpointId: 'vpce-kms-stack'
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1501
|
+
|
|
1502
|
+
// VPC Endpoint SG should be created
|
|
1503
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1504
|
+
|
|
1505
|
+
// CRITICAL: Should use external Lambda SG ID directly, NOT a CloudFormation Ref
|
|
1506
|
+
const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
|
|
1507
|
+
expect(ingressRule.SourceSecurityGroupId).toBe('sg-default-456'); // Direct ID, not { Ref: 'FriggLambdaSecurityGroup' }
|
|
1508
|
+
expect(typeof ingressRule.SourceSecurityGroupId).toBe('string');
|
|
1509
|
+
|
|
1510
|
+
// Verify FriggLambdaSecurityGroup is NOT in the template
|
|
1511
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeUndefined();
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
it('should use CloudFormation Ref when Lambda SG is stack-managed', async () => {
|
|
1515
|
+
const appDefinition = {
|
|
1516
|
+
vpc: {
|
|
1517
|
+
enable: true,
|
|
1518
|
+
enableVPCEndpoints: true,
|
|
1519
|
+
ownership: {
|
|
1520
|
+
securityGroup: 'stack' // Stack-managed Lambda SG
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
1524
|
+
};
|
|
1525
|
+
const discoveredResources = {
|
|
1526
|
+
defaultVpcId: 'vpc-123',
|
|
1527
|
+
privateSubnetId1: 'subnet-1',
|
|
1528
|
+
privateSubnetId2: 'subnet-2'
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1532
|
+
|
|
1533
|
+
// VPC Endpoint SG should be created
|
|
1534
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1535
|
+
|
|
1536
|
+
// Should use CloudFormation Ref when Lambda SG is in stack
|
|
1537
|
+
const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
|
|
1538
|
+
expect(ingressRule.SourceSecurityGroupId).toEqual({ Ref: 'FriggLambdaSecurityGroup' });
|
|
1539
|
+
|
|
1540
|
+
// Verify FriggLambdaSecurityGroup IS in the template
|
|
1541
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1475
1545
|
describe('convertFlatDiscoveryToStructured - VPC Endpoints from CloudFormation', () => {
|
|
1476
1546
|
it('should add VPC endpoints to stackManaged when in existingLogicalIds', () => {
|
|
1477
1547
|
const flatDiscovery = {
|
|
@@ -16,18 +16,20 @@ const { buildEnvironment } = require('../environment-builder');
|
|
|
16
16
|
* Frigg applications need, including:
|
|
17
17
|
* - Core Lambda functions (auth, user, health, dbMigrate)
|
|
18
18
|
* - Error handling infrastructure (SQS, SNS, CloudWatch)
|
|
19
|
-
* - Prisma Lambda Layer
|
|
19
|
+
* - Prisma Lambda Layer (optional, controlled by usePrismaLayer)
|
|
20
20
|
* - Base plugins and esbuild configuration
|
|
21
21
|
*
|
|
22
22
|
* @param {Object} AppDefinition - Application definition
|
|
23
23
|
* @param {Object} appEnvironmentVars - Environment variables from app definition
|
|
24
24
|
* @param {Object} discoveredResources - AWS resources discovered during build
|
|
25
|
+
* @param {boolean} usePrismaLayer - Whether to use Prisma Lambda Layer (default true)
|
|
25
26
|
* @returns {Object} Base serverless definition
|
|
26
27
|
*/
|
|
27
28
|
function createBaseDefinition(
|
|
28
29
|
AppDefinition,
|
|
29
30
|
appEnvironmentVars,
|
|
30
|
-
discoveredResources
|
|
31
|
+
discoveredResources,
|
|
32
|
+
usePrismaLayer = true
|
|
31
33
|
) {
|
|
32
34
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
33
35
|
|
|
@@ -42,11 +44,13 @@ function createBaseDefinition(
|
|
|
42
44
|
: []),
|
|
43
45
|
],
|
|
44
46
|
exclude: [
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Conditionally exclude Prisma (only if using Lambda Layer)
|
|
48
|
+
...(usePrismaLayer ? [
|
|
49
|
+
'node_modules/@prisma/**',
|
|
50
|
+
'node_modules/.prisma/**',
|
|
51
|
+
'node_modules/prisma/**',
|
|
52
|
+
'node_modules/@friggframework/core/generated/**',
|
|
53
|
+
] : []),
|
|
50
54
|
|
|
51
55
|
// Exclude AWS SDK (provided by Lambda runtime)
|
|
52
56
|
'node_modules/aws-sdk/**',
|
|
@@ -218,9 +222,12 @@ function createBaseDefinition(
|
|
|
218
222
|
external: [
|
|
219
223
|
'@aws-sdk/*',
|
|
220
224
|
'aws-sdk',
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
// Conditionally mark Prisma as external (only if using layer)
|
|
226
|
+
...(usePrismaLayer ? [
|
|
227
|
+
'@prisma/client',
|
|
228
|
+
'prisma',
|
|
229
|
+
'.prisma/*',
|
|
230
|
+
] : []),
|
|
224
231
|
],
|
|
225
232
|
packager: 'npm',
|
|
226
233
|
keepNames: true,
|
|
@@ -228,8 +235,11 @@ function createBaseDefinition(
|
|
|
228
235
|
exclude: [
|
|
229
236
|
'aws-sdk',
|
|
230
237
|
'@aws-sdk/*',
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
// Conditionally exclude Prisma (only if using layer)
|
|
239
|
+
...(usePrismaLayer ? [
|
|
240
|
+
'@prisma/client',
|
|
241
|
+
'prisma',
|
|
242
|
+
] : []),
|
|
233
243
|
],
|
|
234
244
|
},
|
|
235
245
|
'serverless-offline': {
|
|
@@ -252,7 +262,7 @@ function createBaseDefinition(
|
|
|
252
262
|
functions: {
|
|
253
263
|
auth: {
|
|
254
264
|
handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
255
|
-
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
265
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
256
266
|
skipEsbuild: true, // Handlers in node_modules don't need bundling
|
|
257
267
|
package: skipEsbuildPackageConfig,
|
|
258
268
|
events: [
|
|
@@ -268,14 +278,14 @@ function createBaseDefinition(
|
|
|
268
278
|
},
|
|
269
279
|
user: {
|
|
270
280
|
handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
|
|
271
|
-
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
281
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
272
282
|
skipEsbuild: true, // Handlers in node_modules don't need bundling
|
|
273
283
|
package: skipEsbuildPackageConfig,
|
|
274
284
|
events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
|
|
275
285
|
},
|
|
276
286
|
health: {
|
|
277
287
|
handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
|
|
278
|
-
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
288
|
+
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
279
289
|
skipEsbuild: true, // Handlers in node_modules don't need bundling
|
|
280
290
|
package: skipEsbuildPackageConfig,
|
|
281
291
|
events: [
|
|
@@ -286,7 +296,7 @@ function createBaseDefinition(
|
|
|
286
296
|
// Note: dbMigrate removed - MigrationBuilder now handles migration infrastructure
|
|
287
297
|
// See: packages/devtools/infrastructure/domains/database/migration-builder.js
|
|
288
298
|
},
|
|
289
|
-
layers: {
|
|
299
|
+
layers: usePrismaLayer ? {
|
|
290
300
|
prisma: {
|
|
291
301
|
path: 'layers/prisma',
|
|
292
302
|
name: '${self:service}-prisma-${sls:stage}',
|
|
@@ -294,7 +304,7 @@ function createBaseDefinition(
|
|
|
294
304
|
compatibleRuntimes: ['nodejs20.x', 'nodejs22.x'],
|
|
295
305
|
retain: false,
|
|
296
306
|
},
|
|
297
|
-
},
|
|
307
|
+
} : {},
|
|
298
308
|
resources: {
|
|
299
309
|
Resources: {
|
|
300
310
|
InternalErrorQueue: {
|
|
@@ -243,6 +243,79 @@ describe('Base Definition Factory', () => {
|
|
|
243
243
|
expect(result.useDotenv).toBeDefined();
|
|
244
244
|
expect(typeof result.useDotenv).toBe('boolean');
|
|
245
245
|
});
|
|
246
|
+
|
|
247
|
+
describe('usePrismaLayer configuration', () => {
|
|
248
|
+
it('should include Prisma layer by default (usePrismaLayer=true)', () => {
|
|
249
|
+
const result = createBaseDefinition({}, {}, {}, true);
|
|
250
|
+
|
|
251
|
+
// Layer definition should exist
|
|
252
|
+
expect(result.layers.prisma).toBeDefined();
|
|
253
|
+
expect(result.layers.prisma.path).toBe('layers/prisma');
|
|
254
|
+
|
|
255
|
+
// Functions should reference the layer
|
|
256
|
+
expect(result.functions.auth.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
257
|
+
expect(result.functions.user.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
258
|
+
expect(result.functions.health.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
259
|
+
|
|
260
|
+
// Prisma should be excluded from packages
|
|
261
|
+
expect(result.functions.auth.package.exclude).toEqual(
|
|
262
|
+
expect.arrayContaining([
|
|
263
|
+
'node_modules/@prisma/**',
|
|
264
|
+
'node_modules/.prisma/**',
|
|
265
|
+
'node_modules/prisma/**',
|
|
266
|
+
'node_modules/@friggframework/core/generated/**',
|
|
267
|
+
])
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Prisma should be external in esbuild
|
|
271
|
+
expect(result.custom.esbuild.external).toContain('@prisma/client');
|
|
272
|
+
expect(result.custom.esbuild.external).toContain('prisma');
|
|
273
|
+
expect(result.custom.esbuild.exclude).toContain('@prisma/client');
|
|
274
|
+
expect(result.custom.esbuild.exclude).toContain('prisma');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should NOT include Prisma layer when usePrismaLayer=false', () => {
|
|
278
|
+
const result = createBaseDefinition({}, {}, {}, false);
|
|
279
|
+
|
|
280
|
+
// Layer definition should NOT exist
|
|
281
|
+
expect(result.layers).toEqual({});
|
|
282
|
+
|
|
283
|
+
// Functions should NOT have layer references
|
|
284
|
+
expect(result.functions.auth.layers).toBeUndefined();
|
|
285
|
+
expect(result.functions.user.layers).toBeUndefined();
|
|
286
|
+
expect(result.functions.health.layers).toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should bundle Prisma with functions when usePrismaLayer=false', () => {
|
|
290
|
+
const result = createBaseDefinition({}, {}, {}, false);
|
|
291
|
+
|
|
292
|
+
// Prisma should NOT be excluded from packages
|
|
293
|
+
expect(result.functions.auth.package.exclude).not.toEqual(
|
|
294
|
+
expect.arrayContaining([
|
|
295
|
+
'node_modules/@prisma/**',
|
|
296
|
+
'node_modules/.prisma/**',
|
|
297
|
+
'node_modules/prisma/**',
|
|
298
|
+
'node_modules/@friggframework/core/generated/**',
|
|
299
|
+
])
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Prisma should NOT be external in esbuild
|
|
303
|
+
expect(result.custom.esbuild.external).not.toContain('@prisma/client');
|
|
304
|
+
expect(result.custom.esbuild.external).not.toContain('prisma');
|
|
305
|
+
expect(result.custom.esbuild.external).not.toContain('.prisma/*');
|
|
306
|
+
expect(result.custom.esbuild.exclude).not.toContain('@prisma/client');
|
|
307
|
+
expect(result.custom.esbuild.exclude).not.toContain('prisma');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should default to usePrismaLayer=true when parameter not provided', () => {
|
|
311
|
+
// Call without 4th parameter
|
|
312
|
+
const result = createBaseDefinition({}, {}, {});
|
|
313
|
+
|
|
314
|
+
// Should behave as if usePrismaLayer=true
|
|
315
|
+
expect(result.layers.prisma).toBeDefined();
|
|
316
|
+
expect(result.functions.auth.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
246
319
|
});
|
|
247
320
|
});
|
|
248
321
|
|
|
@@ -32,8 +32,15 @@ const { validateAndCleanPlugins, validatePackagingConfiguration } = require('./d
|
|
|
32
32
|
const composeServerlessDefinition = async (AppDefinition) => {
|
|
33
33
|
console.log('🏗️ Composing serverless definition with domain builders...');
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
35
|
+
// Determine if using Prisma Lambda Layer (default true)
|
|
36
|
+
const usePrismaLayer = AppDefinition.usePrismaLambdaLayer !== false;
|
|
37
|
+
|
|
38
|
+
// Ensure Prisma layer exists only if using layer mode
|
|
39
|
+
if (usePrismaLayer) {
|
|
40
|
+
await ensurePrismaLayerExists(AppDefinition.database || {});
|
|
41
|
+
} else {
|
|
42
|
+
console.log('📦 Skipping Prisma Lambda Layer (usePrismaLambdaLayer=false - will bundle with functions)');
|
|
43
|
+
}
|
|
37
44
|
|
|
38
45
|
// Create orchestrator with all domain builders
|
|
39
46
|
const orchestrator = new BuilderOrchestrator([
|
|
@@ -55,7 +62,8 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
55
62
|
const definition = createBaseDefinition(
|
|
56
63
|
AppDefinition,
|
|
57
64
|
appEnvironmentVars,
|
|
58
|
-
discoveredResources
|
|
65
|
+
discoveredResources,
|
|
66
|
+
usePrismaLayer
|
|
59
67
|
);
|
|
60
68
|
|
|
61
69
|
// Merge builder results into definition
|
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.490.
|
|
4
|
+
"version": "2.0.0--canary.490.1f4f770.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.490.
|
|
20
|
-
"@friggframework/schemas": "2.0.0--canary.490.
|
|
21
|
-
"@friggframework/test": "2.0.0--canary.490.
|
|
19
|
+
"@friggframework/core": "2.0.0--canary.490.1f4f770.0",
|
|
20
|
+
"@friggframework/schemas": "2.0.0--canary.490.1f4f770.0",
|
|
21
|
+
"@friggframework/test": "2.0.0--canary.490.1f4f770.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.490.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0--canary.490.
|
|
49
|
+
"@friggframework/eslint-config": "2.0.0--canary.490.1f4f770.0",
|
|
50
|
+
"@friggframework/prettier-config": "2.0.0--canary.490.1f4f770.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": "1f4f7704d573f1fe0295cd1d421d5e6b164214e2"
|
|
83
83
|
}
|