@friggframework/devtools 2.0.0--canary.461.e679ea5.0 → 2.0.0--canary.461.d885809.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.
@@ -83,7 +83,36 @@ class AuroraBuilder extends InfrastructureBuilder {
83
83
  console.log(`\n[${this.name}] Configuring Aurora PostgreSQL...`);
84
84
 
85
85
  const dbConfig = appDefinition.database.postgres;
86
- const management = dbConfig.management || 'discover';
86
+
87
+ // Normalize top-level managementMode
88
+ const globalMode = appDefinition.managementMode || 'discover';
89
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
90
+
91
+ let management = dbConfig.management;
92
+
93
+ if (globalMode === 'managed') {
94
+ // Warn about ignored granular options
95
+ if (dbConfig.management) {
96
+ console.log(` ⚠️ managementMode='managed' ignoring: database.postgres.management`);
97
+ }
98
+
99
+ // Clear granular option to prevent conflicts
100
+ delete appDefinition.database.postgres.management;
101
+
102
+ // Set management based on isolation strategy
103
+ if (vpcIsolation === 'isolated') {
104
+ management = 'managed'; // New VPC = new Aurora
105
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → creating new Aurora`);
106
+ } else {
107
+ management = 'discover'; // Shared VPC = reuse Aurora
108
+ appDefinition.database.postgres.autoCreateCredentials = true;
109
+ console.log(` managementMode='managed' + vpcIsolation='shared' → discovering Aurora`);
110
+ }
111
+ } else if (globalMode === 'existing') {
112
+ management = 'existing';
113
+ } else {
114
+ management = management || 'discover';
115
+ }
87
116
 
88
117
  console.log(` PostgreSQL Management Mode: ${management}`);
89
118
 
@@ -749,6 +749,102 @@ describe('AuroraBuilder', () => {
749
749
  });
750
750
  });
751
751
 
752
+ describe('Top-Level Management Mode', () => {
753
+ it('should use managementMode=managed with vpcIsolation=isolated to create new Aurora', async () => {
754
+ const appDefinition = {
755
+ managementMode: 'managed',
756
+ vpcIsolation: 'isolated',
757
+ database: {
758
+ postgres: {
759
+ enable: true,
760
+ management: 'discover', // Should be IGNORED
761
+ minCapacity: 0.5,
762
+ maxCapacity: 1,
763
+ },
764
+ },
765
+ };
766
+
767
+ const discoveredResources = {
768
+ auroraClusterEndpoint: 'existing-cluster.us-east-1.rds.amazonaws.com',
769
+ privateSubnetId1: 'subnet-1',
770
+ privateSubnetId2: 'subnet-2',
771
+ };
772
+
773
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
774
+
775
+ const result = await auroraBuilder.build(appDefinition, discoveredResources);
776
+
777
+ // Should warn about ignored options
778
+ expect(consoleLogSpy).toHaveBeenCalledWith(
779
+ expect.stringContaining("managementMode='managed' ignoring")
780
+ );
781
+
782
+ // Should create new Aurora cluster (isolated mode)
783
+ expect(result.resources.FriggAuroraCluster).toBeDefined();
784
+ expect(result.environment.DATABASE_URL).toBeDefined();
785
+
786
+ consoleLogSpy.mockRestore();
787
+ });
788
+
789
+ it('should use managementMode=managed with vpcIsolation=shared to discover Aurora', async () => {
790
+ const appDefinition = {
791
+ managementMode: 'managed',
792
+ vpcIsolation: 'shared',
793
+ database: {
794
+ postgres: {
795
+ enable: true,
796
+ management: 'managed', // Should be IGNORED
797
+ },
798
+ },
799
+ };
800
+
801
+ const discoveredResources = {
802
+ auroraClusterEndpoint: 'existing-cluster.us-east-1.rds.amazonaws.com',
803
+ auroraClusterPort: 5432,
804
+ auroraClusterIdentifier: 'existing-cluster',
805
+ privateSubnetId1: 'subnet-1',
806
+ privateSubnetId2: 'subnet-2',
807
+ };
808
+
809
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
810
+
811
+ const result = await auroraBuilder.build(appDefinition, discoveredResources);
812
+
813
+ // Should warn about ignored options
814
+ expect(consoleLogSpy).toHaveBeenCalledWith(
815
+ expect.stringContaining("ignoring")
816
+ );
817
+
818
+ // Should discover existing Aurora
819
+ expect(result.resources.FriggAuroraCluster).toBeUndefined();
820
+ expect(result.environment.DATABASE_URL).toBeDefined();
821
+
822
+ consoleLogSpy.mockRestore();
823
+ });
824
+
825
+ it('should respect granular management when no managementMode specified', async () => {
826
+ const appDefinition = {
827
+ // No managementMode
828
+ database: {
829
+ postgres: {
830
+ enable: true,
831
+ management: 'managed', // Should be RESPECTED
832
+ },
833
+ },
834
+ };
835
+
836
+ const discoveredResources = {
837
+ privateSubnetId1: 'subnet-1',
838
+ privateSubnetId2: 'subnet-2',
839
+ };
840
+
841
+ const result = await auroraBuilder.build(appDefinition, discoveredResources);
842
+
843
+ // Should create Aurora cluster
844
+ expect(result.resources.FriggAuroraCluster).toBeDefined();
845
+ });
846
+ });
847
+
752
848
  describe('build() - use-existing mode', () => {
753
849
  it('should use provided database endpoint', async () => {
754
850
  const appDefinition = {
@@ -81,6 +81,21 @@ class VpcBuilder extends InfrastructureBuilder {
81
81
  return result;
82
82
  }
83
83
 
84
+ /**
85
+ * Warn about ignored options when managementMode='managed'
86
+ */
87
+ warnIgnoredOptions(appDefinition) {
88
+ const ignoredOptions = [];
89
+ if (appDefinition.vpc?.management) ignoredOptions.push('vpc.management');
90
+ if (appDefinition.vpc?.subnets?.management) ignoredOptions.push('vpc.subnets.management');
91
+ if (appDefinition.vpc?.natGateway?.management) ignoredOptions.push('vpc.natGateway.management');
92
+ if (appDefinition.vpc?.shareAcrossStages !== undefined) ignoredOptions.push('vpc.shareAcrossStages');
93
+
94
+ if (ignoredOptions.length > 0) {
95
+ console.log(` ⚠️ managementMode='managed' ignoring: ${ignoredOptions.join(', ')}`);
96
+ }
97
+ }
98
+
84
99
  /**
85
100
  * Build complete VPC infrastructure based on management mode
86
101
  */
@@ -110,23 +125,49 @@ class VpcBuilder extends InfrastructureBuilder {
110
125
  Resource: '*',
111
126
  });
112
127
 
113
- // Normalize shareAcrossStages into management mode
114
- // This provides a simpler API for users while maintaining backwards compatibility
128
+ // Normalize top-level managementMode (simplified API)
129
+ const globalMode = appDefinition.managementMode || 'discover';
130
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
131
+
115
132
  let management = appDefinition.vpc.management;
116
- if (!management && appDefinition.vpc.shareAcrossStages !== undefined) {
117
- // Explicit shareAcrossStages setting overrides default
133
+
134
+ if (globalMode === 'managed') {
135
+ // Warn about ignored granular options
136
+ this.warnIgnoredOptions(appDefinition);
137
+
138
+ // Clear granular options to prevent conflicts
139
+ delete appDefinition.vpc.management;
140
+ if (appDefinition.vpc.subnets) delete appDefinition.vpc.subnets.management;
141
+ if (appDefinition.vpc.natGateway) delete appDefinition.vpc.natGateway.management;
142
+ delete appDefinition.vpc.shareAcrossStages;
143
+
144
+ // Set management based on isolation strategy
145
+ if (vpcIsolation === 'isolated') {
146
+ management = 'create-new';
147
+ appDefinition.vpc.natGateway = appDefinition.vpc.natGateway || {};
148
+ appDefinition.vpc.natGateway.management = 'createAndManage';
149
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → creating new VPC`);
150
+ } else {
151
+ management = 'discover';
152
+ appDefinition.vpc.selfHeal = true;
153
+ console.log(` managementMode='managed' + vpcIsolation='shared' → discovering VPC`);
154
+ }
155
+ } else if (globalMode === 'existing') {
156
+ management = 'use-existing';
157
+ } else if (!management && appDefinition.vpc.shareAcrossStages !== undefined) {
158
+ // Legacy shareAcrossStages support (backwards compatibility)
118
159
  management = appDefinition.vpc.shareAcrossStages ? 'discover' : 'create-new';
119
160
  console.log(` VPC Sharing: ${appDefinition.vpc.shareAcrossStages ? 'shared' : 'isolated'} (translated to ${management})`);
120
161
 
121
- // When creating isolated VPC, also create isolated NAT Gateway
122
162
  if (!appDefinition.vpc.shareAcrossStages && !appDefinition.vpc.natGateway?.management) {
123
163
  appDefinition.vpc.natGateway = appDefinition.vpc.natGateway || {};
124
164
  appDefinition.vpc.natGateway.management = 'createAndManage';
125
165
  console.log(` NAT Gateway: creating isolated NAT (shareAcrossStages=false)`);
126
166
  }
127
167
  } else {
128
- management = management || 'discover'; // Default to sharing for backwards compatibility
168
+ management = management || 'discover';
129
169
  }
170
+
130
171
  console.log(` VPC Management Mode: ${management}`);
131
172
 
132
173
  // Handle self-healing if enabled
@@ -809,6 +809,96 @@ describe('VpcBuilder', () => {
809
809
  });
810
810
  });
811
811
 
812
+ describe('Management Mode (Simplified API)', () => {
813
+ it('should use managementMode=managed with vpcIsolation=isolated to create new VPC', async () => {
814
+ const appDefinition = {
815
+ managementMode: 'managed',
816
+ vpcIsolation: 'isolated',
817
+ vpc: {
818
+ enable: true,
819
+ management: 'discover', // Should be IGNORED
820
+ },
821
+ };
822
+
823
+ const discoveredResources = {
824
+ defaultVpcId: 'vpc-existing',
825
+ natGatewayId: 'nat-existing',
826
+ };
827
+
828
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
829
+
830
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
831
+
832
+ // Should warn about ignored options
833
+ expect(consoleLogSpy).toHaveBeenCalledWith(
834
+ expect.stringContaining("managementMode='managed' ignoring")
835
+ );
836
+ expect(consoleLogSpy).toHaveBeenCalledWith(
837
+ expect.stringContaining("vpc.management")
838
+ );
839
+
840
+ // Should create new isolated VPC
841
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
842
+ expect(result.resources.FriggVPC).toBeDefined();
843
+
844
+ // Subnets should use CloudFormation Fn::Cidr
845
+ expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
846
+ 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
847
+ });
848
+
849
+ consoleLogSpy.mockRestore();
850
+ });
851
+
852
+ it('should use managementMode=managed with vpcIsolation=shared to discover VPC', async () => {
853
+ const appDefinition = {
854
+ managementMode: 'managed',
855
+ vpcIsolation: 'shared',
856
+ vpc: {
857
+ enable: true,
858
+ subnets: { management: 'use-existing' }, // Should be IGNORED
859
+ },
860
+ };
861
+
862
+ const discoveredResources = {
863
+ defaultVpcId: 'vpc-existing',
864
+ };
865
+
866
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
867
+
868
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
869
+
870
+ // Should warn about ignored options
871
+ expect(consoleLogSpy).toHaveBeenCalledWith(
872
+ expect.stringContaining("ignoring")
873
+ );
874
+
875
+ // Should discover existing VPC
876
+ expect(result.vpcId).toBe('vpc-existing');
877
+ expect(result.resources.FriggVPC).toBeUndefined();
878
+
879
+ // Should create new stage-specific subnets
880
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
881
+
882
+ consoleLogSpy.mockRestore();
883
+ });
884
+
885
+ it('should default to discover mode for backwards compatibility', async () => {
886
+ const appDefinition = {
887
+ // No managementMode specified
888
+ vpc: {
889
+ enable: true,
890
+ management: 'create-new', // Should be RESPECTED
891
+ },
892
+ };
893
+
894
+ const result = await vpcBuilder.build(appDefinition, {});
895
+
896
+ // Should respect legacy vpc.management
897
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
898
+ expect(result.resources.FriggVPC).toBeDefined();
899
+ });
900
+ });
901
+
812
902
  describe('VPC Sharing Control', () => {
813
903
  it('should share VPC across stages when shareAcrossStages is true (default)', async () => {
814
904
  const appDefinition = {
@@ -828,11 +918,11 @@ describe('VpcBuilder', () => {
828
918
  // Should use discovered VPC (not create new one)
829
919
  expect(result.vpcId).toBe('vpc-shared');
830
920
  expect(result.resources.FriggVPC).toBeUndefined();
831
-
921
+
832
922
  // Should create stage-specific subnets for isolation
833
923
  expect(result.resources.FriggPrivateSubnet1).toBeDefined();
834
924
  expect(result.resources.FriggPrivateSubnet2).toBeDefined();
835
-
925
+
836
926
  // Should reuse discovered NAT Gateway
837
927
  expect(result.resources.FriggNATGateway).toBeUndefined();
838
928
  });
@@ -856,11 +946,11 @@ describe('VpcBuilder', () => {
856
946
  expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
857
947
  expect(result.resources.FriggVPC).toBeDefined();
858
948
  expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
859
-
949
+
860
950
  // Should create stage-specific subnets with Fn::Cidr (dynamic from VPC CIDR)
861
951
  expect(result.resources.FriggPrivateSubnet1).toBeDefined();
862
952
  expect(result.resources.FriggPrivateSubnet2).toBeDefined();
863
-
953
+
864
954
  // Subnets should use CloudFormation Fn::Cidr, NOT hardcoded 172.31.x.x
865
955
  expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
866
956
  'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
@@ -868,7 +958,7 @@ describe('VpcBuilder', () => {
868
958
  expect(result.resources.FriggPrivateSubnet2.Properties.CidrBlock).toEqual({
869
959
  'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
870
960
  });
871
-
961
+
872
962
  // Should create new NAT Gateway
873
963
  expect(result.resources.FriggNATGateway).toBeDefined();
874
964
  });
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.e679ea5.0",
4
+ "version": "2.0.0--canary.461.d885809.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.e679ea5.0",
15
- "@friggframework/test": "2.0.0--canary.461.e679ea5.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.d885809.0",
15
+ "@friggframework/test": "2.0.0--canary.461.d885809.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.e679ea5.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.e679ea5.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.d885809.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.d885809.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": "e679ea5502f29133d25b93b6f5b36f9b3099f82c"
73
+ "gitHead": "d8858090c719e05e3bd548c092844430f728ff05"
74
74
  }