@friggframework/devtools 2.0.0-next.44 → 2.0.0-next.46

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.
Files changed (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +633 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2074
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Tests for BuilderOrchestrator
3
+ *
4
+ * Tests orchestration, dependency resolution, and parallel execution
5
+ */
6
+
7
+ const { BuilderOrchestrator } = require('./builder-orchestrator');
8
+ const { InfrastructureBuilder, ValidationResult } = require('./base-builder');
9
+
10
+ // Mock builders for testing
11
+ class MockBuilderA extends InfrastructureBuilder {
12
+ getName() { return 'MockBuilderA'; }
13
+ shouldExecute(appDef) { return appDef.featureA === true; }
14
+ validate(appDef) { return new ValidationResult(); }
15
+ async build(appDef, discovered) {
16
+ return {
17
+ resources: { ResourceA: { Type: 'Test' } },
18
+ iamStatements: [{ Effect: 'Allow', Action: 'test:A' }],
19
+ };
20
+ }
21
+ }
22
+
23
+ class MockBuilderB extends InfrastructureBuilder {
24
+ getName() { return 'MockBuilderB'; }
25
+ shouldExecute(appDef) { return appDef.featureB === true; }
26
+ validate(appDef) { return new ValidationResult(); }
27
+ getDependencies() { return ['MockBuilderA']; } // Depends on A
28
+ async build(appDef, discovered) {
29
+ return {
30
+ resources: { ResourceB: { Type: 'Test' } },
31
+ environment: { VAR_B: 'value-b' },
32
+ };
33
+ }
34
+ }
35
+
36
+ class MockBuilderC extends InfrastructureBuilder {
37
+ getName() { return 'MockBuilderC'; }
38
+ shouldExecute() { return false; } // Never executes
39
+ validate() { return new ValidationResult(); }
40
+ async build() {
41
+ throw new Error('Should not be called');
42
+ }
43
+ }
44
+
45
+ jest.mock('./resource-discovery', () => ({
46
+ gatherDiscoveredResources: jest.fn().mockResolvedValue({ discovered: true }),
47
+ }));
48
+
49
+ jest.mock('./environment-builder', () => ({
50
+ getAppEnvironmentVars: jest.fn().mockReturnValue({ ENV_VAR: 'value' }),
51
+ buildEnvironment: jest.fn().mockReturnValue({ ENV_VAR: 'value' }),
52
+ }));
53
+
54
+ describe('BuilderOrchestrator', () => {
55
+ let orchestrator;
56
+
57
+ beforeEach(() => {
58
+ jest.clearAllMocks();
59
+ });
60
+
61
+ describe('registerBuilder()', () => {
62
+ it('should register builders', () => {
63
+ orchestrator = new BuilderOrchestrator();
64
+ const builder = new MockBuilderA();
65
+
66
+ orchestrator.registerBuilder(builder);
67
+
68
+ expect(orchestrator.builders.has('MockBuilderA')).toBe(true);
69
+ });
70
+
71
+ it('should register multiple builders via constructor', () => {
72
+ orchestrator = new BuilderOrchestrator([
73
+ new MockBuilderA(),
74
+ new MockBuilderB(),
75
+ ]);
76
+
77
+ expect(orchestrator.builders.size).toBe(2);
78
+ });
79
+ });
80
+
81
+ describe('validateAll()', () => {
82
+ it('should validate all applicable builders', async () => {
83
+ orchestrator = new BuilderOrchestrator([
84
+ new MockBuilderA(),
85
+ new MockBuilderB(),
86
+ ]);
87
+
88
+ const appDef = { featureA: true, featureB: true };
89
+
90
+ await expect(orchestrator.validateAll(appDef)).resolves.toBeDefined();
91
+ });
92
+
93
+ it('should skip builders that shouldNotExecute', async () => {
94
+ orchestrator = new BuilderOrchestrator([
95
+ new MockBuilderA(),
96
+ new MockBuilderC(), // Should not execute
97
+ ]);
98
+
99
+ const appDef = { featureA: true };
100
+
101
+ const results = await orchestrator.validateAll(appDef);
102
+
103
+ // Should only validate MockBuilderA
104
+ expect(results).toHaveLength(1);
105
+ expect(results[0].builder).toBe('MockBuilderA');
106
+ });
107
+
108
+ it('should throw error if validation fails', async () => {
109
+ class FailingBuilder extends InfrastructureBuilder {
110
+ getName() { return 'FailingBuilder'; }
111
+ shouldExecute() { return true; }
112
+ validate() {
113
+ const result = new ValidationResult();
114
+ result.addError('Test error');
115
+ return result;
116
+ }
117
+ }
118
+
119
+ orchestrator = new BuilderOrchestrator([new FailingBuilder()]);
120
+
121
+ await expect(orchestrator.validateAll({})).rejects.toThrow('Infrastructure validation failed');
122
+ });
123
+ });
124
+
125
+ describe('resolveBuildOrder()', () => {
126
+ it('should resolve dependencies correctly', () => {
127
+ orchestrator = new BuilderOrchestrator([
128
+ new MockBuilderB(), // Depends on A
129
+ new MockBuilderA(), // No dependencies
130
+ ]);
131
+
132
+ const appDef = { featureA: true, featureB: true };
133
+ const order = orchestrator.resolveBuildOrder(appDef);
134
+
135
+ // A should come before B
136
+ expect(order).toEqual(['MockBuilderA', 'MockBuilderB']);
137
+ });
138
+
139
+ it('should handle builders with no dependencies', () => {
140
+ orchestrator = new BuilderOrchestrator([
141
+ new MockBuilderA(),
142
+ ]);
143
+
144
+ const appDef = { featureA: true };
145
+ const order = orchestrator.resolveBuildOrder(appDef);
146
+
147
+ expect(order).toEqual(['MockBuilderA']);
148
+ });
149
+
150
+ it('should skip builders that should not execute', () => {
151
+ orchestrator = new BuilderOrchestrator([
152
+ new MockBuilderA(),
153
+ new MockBuilderC(), // Should not execute
154
+ ]);
155
+
156
+ const appDef = { featureA: true };
157
+ const order = orchestrator.resolveBuildOrder(appDef);
158
+
159
+ expect(order).toEqual(['MockBuilderA']);
160
+ });
161
+ });
162
+
163
+ describe('buildAll()', () => {
164
+ it('should build all infrastructure and merge results', async () => {
165
+ orchestrator = new BuilderOrchestrator([
166
+ new MockBuilderA(),
167
+ new MockBuilderB(),
168
+ ]);
169
+
170
+ const appDef = { featureA: true, featureB: true };
171
+
172
+ const result = await orchestrator.buildAll(appDef);
173
+
174
+ expect(result.merged).toBeDefined();
175
+ expect(result.merged.resources).toMatchObject({
176
+ ResourceA: { Type: 'Test' },
177
+ ResourceB: { Type: 'Test' },
178
+ });
179
+ expect(result.merged.iamStatements).toHaveLength(1);
180
+ expect(result.merged.environment).toMatchObject({ VAR_B: 'value-b' });
181
+ });
182
+
183
+ it('should skip builders that should not execute', async () => {
184
+ orchestrator = new BuilderOrchestrator([
185
+ new MockBuilderA(),
186
+ new MockBuilderC(), // Should not execute
187
+ ]);
188
+
189
+ const appDef = { featureA: true };
190
+
191
+ const result = await orchestrator.buildAll(appDef);
192
+
193
+ // Should only have results from A
194
+ expect(Object.keys(result.merged.resources)).toEqual(['ResourceA']);
195
+ });
196
+
197
+ it('should throw error if builder fails', async () => {
198
+ class FailingBuilder extends InfrastructureBuilder {
199
+ getName() { return 'FailingBuilder'; }
200
+ shouldExecute() { return true; }
201
+ validate() { return new ValidationResult(); }
202
+ async build() {
203
+ throw new Error('Build failed');
204
+ }
205
+ }
206
+
207
+ orchestrator = new BuilderOrchestrator([new FailingBuilder()]);
208
+
209
+ await expect(orchestrator.buildAll({})).rejects.toThrow('Build failed');
210
+ });
211
+ });
212
+ });
213
+
@@ -0,0 +1,334 @@
1
+ /**
2
+ * CloudFormation-based Resource Discovery v2
3
+ *
4
+ * Refactored to return structured DiscoveryResult instead of flat object.
5
+ * Part of the clean resource ownership architecture.
6
+ *
7
+ * Domain Service - Hexagonal Architecture
8
+ */
9
+
10
+ const { createEmptyDiscoveryResult } = require('./types');
11
+
12
+ class CloudFormationDiscoveryV2 {
13
+ constructor(provider, config = {}) {
14
+ this.provider = provider;
15
+ this.serviceName = config.serviceName;
16
+ this.stage = config.stage;
17
+ }
18
+
19
+ /**
20
+ * Discover resources from an existing CloudFormation stack
21
+ *
22
+ * @param {string} stackName - Name of the CloudFormation stack
23
+ * @returns {Promise<Object>} DiscoveryResult or empty result if stack doesn't exist
24
+ */
25
+ async discoverFromStack(stackName) {
26
+ try {
27
+ // Try to get the stack
28
+ const stack = await this.provider.describeStack(stackName);
29
+
30
+ // Get stack resources
31
+ const resources = await this.provider.listStackResources(stackName);
32
+
33
+ // Create structured discovery result
34
+ const discovery = createEmptyDiscoveryResult();
35
+ discovery.fromCloudFormation = true;
36
+ discovery.stackName = stackName;
37
+ discovery.region = this.provider.region;
38
+
39
+ // Extract stack-managed resources
40
+ await this._extractStackManagedResources(resources || [], discovery);
41
+
42
+ // Also keep flat structure for backwards compatibility (temporarily)
43
+ const flatDiscovered = {
44
+ fromCloudFormationStack: true,
45
+ stackName: stackName,
46
+ existingLogicalIds: discovery.stackManaged.map(r => r.logicalId)
47
+ };
48
+
49
+ // Extract from outputs (legacy)
50
+ if (stack.Outputs && stack.Outputs.length > 0) {
51
+ this._extractFromOutputs(stack.Outputs, flatDiscovered);
52
+ }
53
+
54
+ // Extract flat properties from stackManaged resources
55
+ this._createFlatPropertiesFromStackManaged(discovery, flatDiscovered);
56
+
57
+ // Return both structures (flat for backwards compat, structured for new code)
58
+ return {
59
+ ...flatDiscovered,
60
+ _structured: discovery // New structured format
61
+ };
62
+ } catch (error) {
63
+ // Stack doesn't exist - return empty discovery
64
+ if (error.message && error.message.includes('does not exist')) {
65
+ const empty = createEmptyDiscoveryResult();
66
+ return {
67
+ fromCloudFormationStack: false,
68
+ _structured: empty
69
+ };
70
+ }
71
+
72
+ // Other errors - log and return empty
73
+ console.warn(`⚠️ CloudFormation discovery failed: ${error.message}`);
74
+ const empty = createEmptyDiscoveryResult();
75
+ return {
76
+ fromCloudFormationStack: false,
77
+ _structured: empty
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Extract stack-managed resources into structured format
84
+ * @private
85
+ */
86
+ async _extractStackManagedResources(resources, discovery) {
87
+ console.log(` DEBUG: Processing ${resources.length} CloudFormation resources...`);
88
+
89
+ for (const resource of resources) {
90
+ const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
91
+
92
+ // Only track Frigg-managed resources
93
+ if (!LogicalResourceId.startsWith('Frigg') && !LogicalResourceId.includes('Migration')) {
94
+ continue;
95
+ }
96
+
97
+ // Add to stack-managed list
98
+ const stackResource = {
99
+ logicalId: LogicalResourceId,
100
+ physicalId: PhysicalResourceId,
101
+ resourceType: ResourceType,
102
+ properties: {} // Will be populated as needed
103
+ };
104
+
105
+ discovery.stackManaged.push(stackResource);
106
+
107
+ // Query AWS for detailed properties for certain resources
108
+ await this._enrichResourceProperties(stackResource, discovery);
109
+ }
110
+
111
+ console.log(` ✓ Discovered ${discovery.stackManaged.length} stack-managed resources`);
112
+ }
113
+
114
+ /**
115
+ * Enrich resource properties by querying AWS APIs
116
+ * @private
117
+ */
118
+ async _enrichResourceProperties(stackResource, discovery) {
119
+ const { logicalId, physicalId, resourceType } = stackResource;
120
+
121
+ // Security Group - query to get VPC ID
122
+ if (logicalId === 'FriggLambdaSecurityGroup' && resourceType === 'AWS::EC2::SecurityGroup') {
123
+ console.log(` ✓ Found security group in stack: ${physicalId}`);
124
+
125
+ if (this.provider && this.provider.getEC2Client) {
126
+ try {
127
+ console.log(` Querying EC2 to get VPC ID from security group...`);
128
+ const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
129
+ const ec2Client = this.provider.getEC2Client();
130
+ const sgDetails = await ec2Client.send(
131
+ new DescribeSecurityGroupsCommand({
132
+ GroupIds: [physicalId]
133
+ })
134
+ );
135
+
136
+ if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
137
+ const sg = sgDetails.SecurityGroups[0];
138
+ stackResource.properties.VpcId = sg.VpcId;
139
+ console.log(` ✓ Extracted VPC ID from security group: ${sg.VpcId}`);
140
+
141
+ // Also add VPC to stack-managed if not already there
142
+ const vpcExists = discovery.stackManaged.some(r => r.logicalId === 'FriggVPC');
143
+ if (!vpcExists && sg.VpcId) {
144
+ // Note: VPC was created by stack, just not listed in resources yet
145
+ // We'll add it when we encounter it, or infer it here
146
+ }
147
+ }
148
+ } catch (error) {
149
+ console.warn(` ⚠️ Could not get VPC from security group: ${error.message}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Aurora Cluster - query to get endpoint
155
+ if (logicalId === 'FriggAuroraCluster' && resourceType === 'AWS::RDS::DBCluster') {
156
+ console.log(` ✓ Found Aurora cluster in stack: ${physicalId}`);
157
+
158
+ if (this.provider) {
159
+ try {
160
+ console.log(` Querying RDS to get Aurora endpoint...`);
161
+ const { DescribeDBClustersCommand, RDSClient } = require('@aws-sdk/client-rds');
162
+
163
+ const rdsClient = new RDSClient({ region: this.provider.region });
164
+ const clusterDetails = await rdsClient.send(
165
+ new DescribeDBClustersCommand({
166
+ DBClusterIdentifier: physicalId
167
+ })
168
+ );
169
+
170
+ if (clusterDetails.DBClusters && clusterDetails.DBClusters.length > 0) {
171
+ const cluster = clusterDetails.DBClusters[0];
172
+ stackResource.properties.Endpoint = cluster.Endpoint;
173
+ stackResource.properties.Port = cluster.Port;
174
+ stackResource.properties.DBClusterIdentifier = cluster.DBClusterIdentifier;
175
+ console.log(` ✓ Extracted Aurora endpoint: ${cluster.Endpoint}:${cluster.Port}`);
176
+ }
177
+ } catch (error) {
178
+ console.warn(` ⚠️ Could not get endpoint from Aurora cluster: ${error.message}`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // KMS Key Alias - query to get ARN
184
+ if (logicalId === 'FriggKMSKeyAlias' && resourceType === 'AWS::KMS::Alias') {
185
+ console.log(` ✓ Found KMS key alias in stack: ${physicalId}`);
186
+
187
+ if (this.provider && this.provider.describeKmsKey) {
188
+ try {
189
+ console.log(` Querying KMS for alias: ${physicalId}...`);
190
+ const keyMetadata = await this.provider.describeKmsKey(physicalId);
191
+
192
+ if (keyMetadata) {
193
+ stackResource.properties.KeyArn = keyMetadata.Arn;
194
+ console.log(` ✓ Found KMS key via alias query: ${keyMetadata.Arn}`);
195
+ }
196
+ } catch (error) {
197
+ console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ // VPC - just log
203
+ if (logicalId === 'FriggVPC' && resourceType === 'AWS::EC2::VPC') {
204
+ console.log(` ✓ Found VPC in stack: ${physicalId}`);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Create flat properties from stack-managed resources (backwards compatibility)
210
+ * @private
211
+ */
212
+ _createFlatPropertiesFromStackManaged(discovery, flatDiscovered) {
213
+ for (const resource of discovery.stackManaged) {
214
+ const { logicalId, physicalId, properties } = resource;
215
+
216
+ // Map to flat property names
217
+ switch (logicalId) {
218
+ case 'FriggVPC':
219
+ flatDiscovered.defaultVpcId = physicalId;
220
+ break;
221
+ case 'FriggLambdaSecurityGroup':
222
+ flatDiscovered.securityGroupId = physicalId;
223
+ if (properties.VpcId) {
224
+ flatDiscovered.defaultVpcId = properties.VpcId;
225
+ }
226
+ break;
227
+ case 'FriggPrivateSubnet1':
228
+ flatDiscovered.privateSubnetId1 = physicalId;
229
+ break;
230
+ case 'FriggPrivateSubnet2':
231
+ flatDiscovered.privateSubnetId2 = physicalId;
232
+ break;
233
+ case 'FriggPublicSubnet':
234
+ flatDiscovered.publicSubnetId1 = physicalId;
235
+ break;
236
+ case 'FriggPublicSubnet2':
237
+ flatDiscovered.publicSubnetId2 = physicalId;
238
+ break;
239
+ case 'FriggNatGateway':
240
+ flatDiscovered.natGatewayId = physicalId;
241
+ break;
242
+ case 'FriggLambdaRouteTable':
243
+ flatDiscovered.routeTableId = physicalId;
244
+ break;
245
+ case 'FriggVPCEndpointSecurityGroup':
246
+ flatDiscovered.vpcEndpointSecurityGroupId = physicalId;
247
+ break;
248
+ case 'FriggS3VPCEndpoint':
249
+ flatDiscovered.s3VpcEndpointId = physicalId;
250
+ break;
251
+ case 'FriggDynamoDBVPCEndpoint':
252
+ flatDiscovered.dynamoDbVpcEndpointId = physicalId;
253
+ break;
254
+ case 'FriggKMSVPCEndpoint':
255
+ flatDiscovered.kmsVpcEndpointId = physicalId;
256
+ break;
257
+ case 'FriggSecretsManagerVPCEndpoint':
258
+ flatDiscovered.secretsManagerVpcEndpointId = physicalId;
259
+ break;
260
+ case 'FriggSQSVPCEndpoint':
261
+ flatDiscovered.sqsVpcEndpointId = physicalId;
262
+ break;
263
+ case 'FriggAuroraCluster':
264
+ flatDiscovered.auroraClusterId = physicalId;
265
+ if (properties.Endpoint) {
266
+ flatDiscovered.auroraClusterEndpoint = properties.Endpoint;
267
+ }
268
+ if (properties.Port) {
269
+ flatDiscovered.auroraClusterPort = properties.Port;
270
+ flatDiscovered.auroraPort = properties.Port;
271
+ }
272
+ if (properties.DBClusterIdentifier) {
273
+ flatDiscovered.auroraClusterIdentifier = properties.DBClusterIdentifier;
274
+ }
275
+ break;
276
+ case 'FriggKMSKey':
277
+ flatDiscovered.defaultKmsKeyId = physicalId;
278
+ break;
279
+ case 'FriggKMSKeyAlias':
280
+ flatDiscovered.kmsKeyAlias = physicalId;
281
+ if (properties.KeyArn) {
282
+ flatDiscovered.defaultKmsKeyId = properties.KeyArn;
283
+ }
284
+ break;
285
+ case 'FriggMigrationStatusBucket':
286
+ flatDiscovered.migrationStatusBucket = physicalId;
287
+ break;
288
+ case 'DbMigrationQueue':
289
+ flatDiscovered.migrationQueueUrl = physicalId;
290
+ break;
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Extract discovered resources from CloudFormation stack outputs (legacy)
297
+ * @private
298
+ */
299
+ _extractFromOutputs(outputs, discovered) {
300
+ const outputMap = outputs.reduce((acc, output) => {
301
+ acc[output.OutputKey] = output.OutputValue;
302
+ return acc;
303
+ }, {});
304
+
305
+ // VPC outputs
306
+ if (outputMap.VpcId) {
307
+ discovered.defaultVpcId = outputMap.VpcId;
308
+ }
309
+
310
+ if (outputMap.PrivateSubnetIds) {
311
+ discovered.privateSubnetIds = outputMap.PrivateSubnetIds.split(',').map(id => id.trim());
312
+ }
313
+
314
+ if (outputMap.PublicSubnetId) {
315
+ discovered.publicSubnetId = outputMap.PublicSubnetId;
316
+ }
317
+
318
+ if (outputMap.SecurityGroupId) {
319
+ discovered.securityGroupId = outputMap.SecurityGroupId;
320
+ }
321
+
322
+ // KMS outputs
323
+ if (outputMap.KMS_KEY_ARN) {
324
+ discovered.defaultKmsKeyId = outputMap.KMS_KEY_ARN;
325
+ }
326
+
327
+ // Database outputs
328
+ if (outputMap.DatabaseEndpoint) {
329
+ discovered.databaseEndpoint = outputMap.DatabaseEndpoint;
330
+ }
331
+ }
332
+ }
333
+
334
+ module.exports = CloudFormationDiscoveryV2;