@friggframework/devtools 2.0.0--canary.461.4860820.0 → 2.0.0--canary.461.12ba2eb.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/database/aurora-builder.js +39 -6
- package/infrastructure/domains/database/aurora-builder.test.js +49 -9
- package/infrastructure/domains/database/migration-builder.js +57 -0
- package/infrastructure/domains/database/migration-builder.test.js +38 -0
- package/package.json +6 -6
|
@@ -277,7 +277,7 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
277
277
|
// Check if we should auto-create credentials
|
|
278
278
|
if (dbConfig.autoCreateCredentials && !discoveredResources.databaseSecretArn) {
|
|
279
279
|
console.log(' Creating Secrets Manager secret and rotating Aurora password...');
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// Create Secrets Manager secret with auto-generated password
|
|
282
282
|
result.resources.FriggDBSecret = {
|
|
283
283
|
Type: 'AWS::SecretsManager::Secret',
|
|
@@ -473,15 +473,15 @@ exports.handler = async (event, context) => {
|
|
|
473
473
|
// No secret and no auto-create - set individual DB connection components
|
|
474
474
|
// The application will construct DATABASE_URL at runtime from these components + DATABASE_USER + DATABASE_PASSWORD
|
|
475
475
|
const dbName = dbConfig.database || 'frigg';
|
|
476
|
-
|
|
476
|
+
|
|
477
477
|
result.environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
478
478
|
result.environment.DATABASE_PORT = String(discoveredResources.auroraPort || 5432);
|
|
479
479
|
result.environment.DATABASE_NAME = dbName;
|
|
480
|
-
|
|
480
|
+
|
|
481
481
|
// Note: DATABASE_URL is NOT set here to avoid Serverless variable resolution errors
|
|
482
482
|
// The application (Frigg Core) should construct it at runtime from:
|
|
483
483
|
// DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
|
|
484
|
-
|
|
484
|
+
|
|
485
485
|
console.log(' ℹ️ No Secrets Manager secret found - set DATABASE_USER and DATABASE_PASSWORD in Lambda environment');
|
|
486
486
|
console.log(' ℹ️ Application will construct DATABASE_URL at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD');
|
|
487
487
|
console.log(' ℹ️ Or enable autoCreateCredentials=true to automatically create and rotate credentials');
|
|
@@ -508,14 +508,47 @@ exports.handler = async (event, context) => {
|
|
|
508
508
|
|
|
509
509
|
/**
|
|
510
510
|
* Build DATABASE_URL connection string
|
|
511
|
+
* @param {string|object} host - Database host (string or CloudFormation intrinsic function)
|
|
512
|
+
* @param {string|number|object} port - Database port (string/number or CloudFormation intrinsic function)
|
|
513
|
+
* @param {string} database - Database name
|
|
514
|
+
* @param {string|object} secretRef - Secret ARN (string) or CloudFormation Ref object
|
|
511
515
|
*/
|
|
512
516
|
buildDatabaseUrl(host, port, database, secretRef) {
|
|
517
|
+
// Handle secretRef as either a string ARN or CloudFormation Ref object
|
|
518
|
+
const resolveSecretRef = (secretRefValue) => {
|
|
519
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
520
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
521
|
+
return {
|
|
522
|
+
'Fn::Sub': [
|
|
523
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:username}}',
|
|
524
|
+
{ SecretArn: secretRefValue },
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// String ARN - use directly
|
|
529
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:username}}`;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const resolveSecretPassword = (secretRefValue) => {
|
|
533
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
534
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
535
|
+
return {
|
|
536
|
+
'Fn::Sub': [
|
|
537
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:password}}',
|
|
538
|
+
{ SecretArn: secretRefValue },
|
|
539
|
+
],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// String ARN - use directly
|
|
543
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:password}}`;
|
|
544
|
+
};
|
|
545
|
+
|
|
513
546
|
return {
|
|
514
547
|
'Fn::Sub': [
|
|
515
548
|
`postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}`,
|
|
516
549
|
{
|
|
517
|
-
Username:
|
|
518
|
-
Password:
|
|
550
|
+
Username: resolveSecretRef(secretRef),
|
|
551
|
+
Password: resolveSecretPassword(secretRef),
|
|
519
552
|
Host: host,
|
|
520
553
|
Port: port,
|
|
521
554
|
Database: database,
|
|
@@ -388,8 +388,14 @@ describe('AuroraBuilder', () => {
|
|
|
388
388
|
// Check DATABASE_URL uses the secret
|
|
389
389
|
expect(result.environment.DATABASE_URL).toBeDefined();
|
|
390
390
|
expect(result.environment.DATABASE_URL['Fn::Sub']).toBeDefined();
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
|
|
392
|
+
// Username and Password should use nested Fn::Sub to resolve the Ref
|
|
393
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Username['Fn::Sub']).toBeDefined();
|
|
394
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Password['Fn::Sub']).toBeDefined();
|
|
395
|
+
|
|
396
|
+
// Should contain secretsmanager resolution
|
|
397
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Username['Fn::Sub'][0]).toContain('resolve:secretsmanager');
|
|
398
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Password['Fn::Sub'][0]).toContain('resolve:secretsmanager');
|
|
393
399
|
|
|
394
400
|
// Check IAM permissions for secret access
|
|
395
401
|
const secretPermission = result.iamStatements.find(stmt =>
|
|
@@ -426,11 +432,11 @@ describe('AuroraBuilder', () => {
|
|
|
426
432
|
expect(result.environment.DATABASE_HOST).toBe('cluster.abc.us-east-1.rds.amazonaws.com');
|
|
427
433
|
expect(result.environment.DATABASE_PORT).toBe('5432');
|
|
428
434
|
expect(result.environment.DATABASE_NAME).toBe('frigg');
|
|
429
|
-
|
|
435
|
+
|
|
430
436
|
// DATABASE_URL should NOT be set (to avoid Serverless variable resolution errors)
|
|
431
437
|
// The application should construct it at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
|
|
432
438
|
expect(result.environment.DATABASE_URL).toBeUndefined();
|
|
433
|
-
|
|
439
|
+
|
|
434
440
|
// DATABASE_USER and DATABASE_PASSWORD should come from appDefinition.environment
|
|
435
441
|
// and will be set by the environment-builder, not here
|
|
436
442
|
});
|
|
@@ -523,15 +529,49 @@ describe('AuroraBuilder', () => {
|
|
|
523
529
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
524
530
|
|
|
525
531
|
const zipFileCode = result.resources.PasswordRotatorLambda.Properties.Code.ZipFile;
|
|
526
|
-
|
|
532
|
+
|
|
527
533
|
// Should not contain template literals that would conflict with CloudFormation ${} substitution
|
|
528
534
|
// CloudFormation uses ${} for parameter substitution, so we should avoid `${variable}` in ZipFile
|
|
529
535
|
expect(zipFileCode).not.toMatch(/`.*\$\{(?!env:).*\}`/); // No template literals with ${} except ${env:...}
|
|
530
|
-
|
|
536
|
+
|
|
531
537
|
// Should use string concatenation instead
|
|
532
538
|
expect(zipFileCode).toContain("'Successfully rotated password for cluster: ' + ClusterIdentifier");
|
|
533
539
|
});
|
|
534
540
|
|
|
541
|
+
it('should properly handle Ref objects in buildDatabaseUrl when autoCreateCredentials is enabled', async () => {
|
|
542
|
+
const appDefinition = {
|
|
543
|
+
database: {
|
|
544
|
+
postgres: {
|
|
545
|
+
enable: true,
|
|
546
|
+
management: 'discover',
|
|
547
|
+
autoCreateCredentials: true,
|
|
548
|
+
database: 'testdb',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const discoveredResources = {
|
|
554
|
+
auroraClusterEndpoint: 'cluster.abc.us-east-1.rds.amazonaws.com',
|
|
555
|
+
auroraPort: 5432,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
559
|
+
|
|
560
|
+
const dbUrl = result.environment.DATABASE_URL;
|
|
561
|
+
|
|
562
|
+
// Should use Fn::Sub with nested Fn::Sub to resolve the Ref
|
|
563
|
+
expect(dbUrl['Fn::Sub']).toBeDefined();
|
|
564
|
+
expect(dbUrl['Fn::Sub'][0]).toBe('postgresql://${Username}:${Password}@${Host}:${Port}/${Database}');
|
|
565
|
+
|
|
566
|
+
// The Username and Password should use Fn::Sub to resolve the secret Ref, not literal "[object Object]"
|
|
567
|
+
expect(dbUrl['Fn::Sub'][1].Username['Fn::Sub']).toBeDefined();
|
|
568
|
+
expect(dbUrl['Fn::Sub'][1].Password['Fn::Sub']).toBeDefined();
|
|
569
|
+
|
|
570
|
+
// Should not contain the literal string "[object Object]"
|
|
571
|
+
const jsonOutput = JSON.stringify(dbUrl);
|
|
572
|
+
expect(jsonOutput).not.toContain('[object Object]');
|
|
573
|
+
});
|
|
574
|
+
|
|
535
575
|
it('should properly escape ExcludeCharacters for valid JSON in CloudFormation template', async () => {
|
|
536
576
|
const appDefinition = {
|
|
537
577
|
database: {
|
|
@@ -551,15 +591,15 @@ describe('AuroraBuilder', () => {
|
|
|
551
591
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
552
592
|
|
|
553
593
|
const excludeChars = result.resources.FriggDBSecret.Properties.GenerateSecretString.ExcludeCharacters;
|
|
554
|
-
|
|
594
|
+
|
|
555
595
|
// Should properly escape the backslash so it's valid JSON
|
|
556
596
|
// In JavaScript string: '"@/\\' represents the string: "@/\
|
|
557
597
|
// When serialized to JSON, backslash must be doubled: '"@/\\'
|
|
558
598
|
expect(excludeChars).toBe('"@/\\\\');
|
|
559
|
-
|
|
599
|
+
|
|
560
600
|
// Verify it can be JSON-stringified without errors
|
|
561
601
|
expect(() => JSON.stringify(result.resources.FriggDBSecret)).not.toThrow();
|
|
562
|
-
|
|
602
|
+
|
|
563
603
|
// Verify the JSON output has the correct escape sequence
|
|
564
604
|
const jsonOutput = JSON.stringify(result.resources.FriggDBSecret);
|
|
565
605
|
expect(jsonOutput).toContain('\\"@/\\\\\\\\'); // In JSON string: "\"@/\\\\"
|
|
@@ -72,6 +72,61 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
72
72
|
|
|
73
73
|
console.log(' ✓ Created DbMigrationQueue resource');
|
|
74
74
|
|
|
75
|
+
// Package configuration for migration functions (reuse from base-definition-factory)
|
|
76
|
+
const migrationPackageConfig = {
|
|
77
|
+
individually: true,
|
|
78
|
+
exclude: [
|
|
79
|
+
// Exclude ALL nested node_modules
|
|
80
|
+
'node_modules/**/node_modules/**',
|
|
81
|
+
'node_modules/aws-sdk/**',
|
|
82
|
+
'node_modules/@aws-sdk/**',
|
|
83
|
+
'node_modules/esbuild/**',
|
|
84
|
+
'node_modules/@esbuild/**',
|
|
85
|
+
'node_modules/typescript/**',
|
|
86
|
+
'node_modules/webpack/**',
|
|
87
|
+
'node_modules/osls/**',
|
|
88
|
+
'node_modules/serverless-esbuild/**',
|
|
89
|
+
'node_modules/serverless-jetpack/**',
|
|
90
|
+
'node_modules/serverless-offline/**',
|
|
91
|
+
'node_modules/serverless-offline-sqs/**',
|
|
92
|
+
'node_modules/serverless-dotenv-plugin/**',
|
|
93
|
+
'node_modules/serverless-kms-grants/**',
|
|
94
|
+
'node_modules/@friggframework/test/**',
|
|
95
|
+
'node_modules/@friggframework/eslint-config/**',
|
|
96
|
+
'node_modules/@friggframework/prettier-config/**',
|
|
97
|
+
'node_modules/@friggframework/devtools/**',
|
|
98
|
+
'node_modules/@friggframework/serverless-plugin/**',
|
|
99
|
+
'node_modules/jest/**',
|
|
100
|
+
'node_modules/prettier/**',
|
|
101
|
+
'node_modules/eslint/**',
|
|
102
|
+
'node_modules/@friggframework/core/generated/prisma-mongodb/**',
|
|
103
|
+
'node_modules/@friggframework/core/integrations/**',
|
|
104
|
+
'node_modules/@friggframework/core/user/**',
|
|
105
|
+
'node_modules/@friggframework/core/handlers/routers/**',
|
|
106
|
+
'**/query-engine-darwin*',
|
|
107
|
+
'**/schema-engine-darwin*',
|
|
108
|
+
'**/libquery_engine-darwin*',
|
|
109
|
+
'**/*-darwin-arm64*',
|
|
110
|
+
'**/*-darwin*',
|
|
111
|
+
'**/runtime/*.wasm',
|
|
112
|
+
'**/*.wasm*',
|
|
113
|
+
'src/**',
|
|
114
|
+
'test/**',
|
|
115
|
+
'layers/**',
|
|
116
|
+
'coverage/**',
|
|
117
|
+
'deploy.log',
|
|
118
|
+
'.env.backup',
|
|
119
|
+
'docker-compose.yml',
|
|
120
|
+
'jest.config.js',
|
|
121
|
+
'jest.unit.config.js',
|
|
122
|
+
'package-lock.json',
|
|
123
|
+
'**/*.test.js',
|
|
124
|
+
'**/*.spec.js',
|
|
125
|
+
'**/.claude-flow/**',
|
|
126
|
+
'**/.swarm/**',
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
75
130
|
// Create migration worker Lambda (triggered by SQS)
|
|
76
131
|
result.functions.dbMigrationWorker = {
|
|
77
132
|
handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
|
|
@@ -81,6 +136,7 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
81
136
|
memorySize: 1024, // Extra memory for Prisma operations
|
|
82
137
|
reservedConcurrency: 1, // Process one migration at a time (critical for safety)
|
|
83
138
|
description: 'Database migration worker (triggered by SQS queue)',
|
|
139
|
+
package: migrationPackageConfig,
|
|
84
140
|
events: [
|
|
85
141
|
{
|
|
86
142
|
sqs: {
|
|
@@ -101,6 +157,7 @@ class MigrationBuilder extends InfrastructureBuilder {
|
|
|
101
157
|
timeout: 30, // Router just queues jobs, doesn't run migrations
|
|
102
158
|
memorySize: 512,
|
|
103
159
|
description: 'Database migration HTTP API (POST to trigger, GET to check status)',
|
|
160
|
+
package: migrationPackageConfig,
|
|
104
161
|
events: [
|
|
105
162
|
{ httpApi: { path: '/db-migrate', method: 'POST' } },
|
|
106
163
|
{ httpApi: { path: '/db-migrate/{processId}', method: 'GET' } },
|
|
@@ -172,6 +172,44 @@ describe('MigrationBuilder', () => {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
it('should configure package exclusions for migration functions to reduce Lambda size', async () => {
|
|
176
|
+
const appDef = {
|
|
177
|
+
database: {
|
|
178
|
+
postgres: {
|
|
179
|
+
enable: true,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = await builder.build(appDef, {});
|
|
185
|
+
|
|
186
|
+
// Both migration functions should have package configs
|
|
187
|
+
expect(result.functions.dbMigrationWorker.package).toBeDefined();
|
|
188
|
+
expect(result.functions.dbMigrationRouter.package).toBeDefined();
|
|
189
|
+
|
|
190
|
+
// Check worker package config
|
|
191
|
+
const workerPackage = result.functions.dbMigrationWorker.package;
|
|
192
|
+
expect(workerPackage.individually).toBe(true);
|
|
193
|
+
expect(workerPackage.exclude).toBeDefined();
|
|
194
|
+
expect(Array.isArray(workerPackage.exclude)).toBe(true);
|
|
195
|
+
|
|
196
|
+
// Verify critical exclusions for size optimization
|
|
197
|
+
expect(workerPackage.exclude).toContain('test/**');
|
|
198
|
+
expect(workerPackage.exclude).toContain('**/*.test.js');
|
|
199
|
+
expect(workerPackage.exclude).toContain('node_modules/**/node_modules/**');
|
|
200
|
+
expect(workerPackage.exclude).toContain('node_modules/esbuild/**');
|
|
201
|
+
expect(workerPackage.exclude).toContain('node_modules/typescript/**');
|
|
202
|
+
expect(workerPackage.exclude).toContain('node_modules/@friggframework/devtools/**');
|
|
203
|
+
expect(workerPackage.exclude).toContain('src/**'); // Migration handlers don't need backend source
|
|
204
|
+
|
|
205
|
+
// Check router package config
|
|
206
|
+
const routerPackage = result.functions.dbMigrationRouter.package;
|
|
207
|
+
expect(routerPackage.individually).toBe(true);
|
|
208
|
+
expect(routerPackage.exclude).toBeDefined();
|
|
209
|
+
expect(routerPackage.exclude).toContain('test/**');
|
|
210
|
+
expect(routerPackage.exclude).toContain('node_modules/**/node_modules/**');
|
|
211
|
+
});
|
|
212
|
+
|
|
175
213
|
it('should add queue URL to environment', async () => {
|
|
176
214
|
const appDef = {
|
|
177
215
|
database: {
|
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.461.
|
|
4
|
+
"version": "2.0.0--canary.461.12ba2eb.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@babel/eslint-parser": "^7.18.9",
|
|
12
12
|
"@babel/parser": "^7.25.3",
|
|
13
13
|
"@babel/traverse": "^7.25.3",
|
|
14
|
-
"@friggframework/schemas": "2.0.0--canary.461.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.461.
|
|
14
|
+
"@friggframework/schemas": "2.0.0--canary.461.12ba2eb.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.461.12ba2eb.0",
|
|
16
16
|
"@hapi/boom": "^10.0.1",
|
|
17
17
|
"@inquirer/prompts": "^5.3.8",
|
|
18
18
|
"axios": "^1.7.2",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"serverless-http": "^2.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@friggframework/eslint-config": "2.0.0--canary.461.
|
|
38
|
-
"@friggframework/prettier-config": "2.0.0--canary.461.
|
|
37
|
+
"@friggframework/eslint-config": "2.0.0--canary.461.12ba2eb.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.12ba2eb.0",
|
|
39
39
|
"aws-sdk-client-mock": "^4.1.0",
|
|
40
40
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
41
41
|
"jest": "^30.1.3",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "12ba2eb2888f4999e52cf4b8f6eadc9f1beacfb0"
|
|
74
74
|
}
|