@friggframework/devtools 2.0.0--canary.461.5fd8136.0 → 2.0.0--canary.461.9de6fdd.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.
@@ -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',
@@ -470,31 +470,20 @@ exports.handler = async (event, context) => {
470
470
 
471
471
  console.log(' ✅ Using discovered Secrets Manager credentials');
472
472
  } else {
473
- // No secret and no auto-create - construct DATABASE_URL using environment variables at runtime
473
+ // No secret and no auto-create - set individual DB connection components
474
+ // The application will construct DATABASE_URL at runtime from these components + DATABASE_USER + DATABASE_PASSWORD
474
475
  const dbName = dbConfig.database || 'frigg';
475
-
476
- // Set individual environment variables for flexible credential management
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
-
481
- // Build DATABASE_URL using CloudFormation intrinsic functions to reference
482
- // the environment variables at runtime (not build time)
483
- result.environment.DATABASE_URL = {
484
- 'Fn::Sub': [
485
- 'postgresql://${DatabaseUser}:${DatabasePassword}@${DatabaseHost}:${DatabasePort}/${DatabaseName}',
486
- {
487
- DatabaseUser: '${env:DATABASE_USER, "postgres"}',
488
- DatabasePassword: '${env:DATABASE_PASSWORD}',
489
- DatabaseHost: discoveredResources.auroraClusterEndpoint,
490
- DatabasePort: String(discoveredResources.auroraPort || 5432),
491
- DatabaseName: dbName,
492
- },
493
- ],
494
- };
495
-
496
- console.log(' ℹ️ No Secrets Manager secret found - DATABASE_URL will use DATABASE_USER and DATABASE_PASSWORD from environment');
497
- console.log(' ℹ️ Set DATABASE_USER and DATABASE_PASSWORD in Lambda environment or via serverless deploy --param');
480
+
481
+ // Note: DATABASE_URL is NOT set here to avoid Serverless variable resolution errors
482
+ // The application (Frigg Core) should construct it at runtime from:
483
+ // DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
484
+
485
+ console.log(' ℹ️ No Secrets Manager secret found - set DATABASE_USER and DATABASE_PASSWORD in Lambda environment');
486
+ console.log(' ℹ️ Application will construct DATABASE_URL at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD');
498
487
  console.log(' ℹ️ Or enable autoCreateCredentials=true to automatically create and rotate credentials');
499
488
  }
500
489
 
@@ -519,14 +508,47 @@ exports.handler = async (event, context) => {
519
508
 
520
509
  /**
521
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
522
515
  */
523
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
+
524
546
  return {
525
547
  'Fn::Sub': [
526
548
  `postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}`,
527
549
  {
528
- Username: `{{resolve:secretsmanager:${secretRef}:SecretString:username}}`,
529
- Password: `{{resolve:secretsmanager:${secretRef}:SecretString:password}}`,
550
+ Username: resolveSecretRef(secretRef),
551
+ Password: resolveSecretPassword(secretRef),
530
552
  Host: host,
531
553
  Port: port,
532
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
- expect(result.environment.DATABASE_URL['Fn::Sub'][1].Username).toContain('resolve:secretsmanager');
392
- expect(result.environment.DATABASE_URL['Fn::Sub'][1].Password).toContain('resolve:secretsmanager');
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 =>
@@ -422,11 +428,17 @@ describe('AuroraBuilder', () => {
422
428
  expect(result.resources.FriggAuroraPasswordRotator).toBeUndefined();
423
429
  expect(result.resources.PasswordRotatorRole).toBeUndefined();
424
430
 
425
- // DATABASE_URL should use env variables
426
- expect(result.environment.DATABASE_URL).toBeDefined();
427
- expect(result.environment.DATABASE_URL['Fn::Sub']).toBeDefined();
428
- expect(result.environment.DATABASE_URL['Fn::Sub'][1].DatabaseUser).toContain('env:DATABASE_USER');
429
- expect(result.environment.DATABASE_URL['Fn::Sub'][1].DatabasePassword).toContain('env:DATABASE_PASSWORD');
431
+ // Should set individual environment variables for flexible credential management
432
+ expect(result.environment.DATABASE_HOST).toBe('cluster.abc.us-east-1.rds.amazonaws.com');
433
+ expect(result.environment.DATABASE_PORT).toBe('5432');
434
+ expect(result.environment.DATABASE_NAME).toBe('frigg');
435
+
436
+ // DATABASE_URL should NOT be set (to avoid Serverless variable resolution errors)
437
+ // The application should construct it at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
438
+ expect(result.environment.DATABASE_URL).toBeUndefined();
439
+
440
+ // DATABASE_USER and DATABASE_PASSWORD should come from appDefinition.environment
441
+ // and will be set by the environment-builder, not here
430
442
  });
431
443
 
432
444
  it('should not create credentials when secret is already discovered', async () => {
@@ -517,15 +529,49 @@ describe('AuroraBuilder', () => {
517
529
  const result = await auroraBuilder.build(appDefinition, discoveredResources);
518
530
 
519
531
  const zipFileCode = result.resources.PasswordRotatorLambda.Properties.Code.ZipFile;
520
-
532
+
521
533
  // Should not contain template literals that would conflict with CloudFormation ${} substitution
522
534
  // CloudFormation uses ${} for parameter substitution, so we should avoid `${variable}` in ZipFile
523
535
  expect(zipFileCode).not.toMatch(/`.*\$\{(?!env:).*\}`/); // No template literals with ${} except ${env:...}
524
-
536
+
525
537
  // Should use string concatenation instead
526
538
  expect(zipFileCode).toContain("'Successfully rotated password for cluster: ' + ClusterIdentifier");
527
539
  });
528
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
+
529
575
  it('should properly escape ExcludeCharacters for valid JSON in CloudFormation template', async () => {
530
576
  const appDefinition = {
531
577
  database: {
@@ -545,15 +591,15 @@ describe('AuroraBuilder', () => {
545
591
  const result = await auroraBuilder.build(appDefinition, discoveredResources);
546
592
 
547
593
  const excludeChars = result.resources.FriggDBSecret.Properties.GenerateSecretString.ExcludeCharacters;
548
-
594
+
549
595
  // Should properly escape the backslash so it's valid JSON
550
596
  // In JavaScript string: '"@/\\' represents the string: "@/\
551
597
  // When serialized to JSON, backslash must be doubled: '"@/\\'
552
598
  expect(excludeChars).toBe('"@/\\\\');
553
-
599
+
554
600
  // Verify it can be JSON-stringified without errors
555
601
  expect(() => JSON.stringify(result.resources.FriggDBSecret)).not.toThrow();
556
-
602
+
557
603
  // Verify the JSON output has the correct escape sequence
558
604
  const jsonOutput = JSON.stringify(result.resources.FriggDBSecret);
559
605
  expect(jsonOutput).toContain('\\"@/\\\\\\\\'); // In JSON string: "\"@/\\\\"
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.5fd8136.0",
4
+ "version": "2.0.0--canary.461.9de6fdd.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.5fd8136.0",
15
- "@friggframework/test": "2.0.0--canary.461.5fd8136.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.9de6fdd.0",
15
+ "@friggframework/test": "2.0.0--canary.461.9de6fdd.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.5fd8136.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.5fd8136.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.9de6fdd.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.9de6fdd.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": "5fd8136c7179570f39ae5ebf4c6c195eb217caa5"
73
+ "gitHead": "9de6fddc9112148ee8c11b4c4873371c806a576c"
74
74
  }