@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47

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 +695 -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 -2094
  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,784 @@
1
+ /**
2
+ * AWSPropertyReconciler - AWS Property Drift Reconciliation Adapter
3
+ *
4
+ * Infrastructure Adapter - Hexagonal Architecture
5
+ *
6
+ * Implements IPropertyReconciler port for AWS.
7
+ * Handles property drift reconciliation via template or resource updates.
8
+ *
9
+ * Lazy-loads AWS SDK to minimize cold start time and memory usage.
10
+ */
11
+
12
+ const IPropertyReconciler = require('../../application/ports/IPropertyReconciler');
13
+
14
+ // Lazy-loaded AWS SDK clients
15
+ let CloudFormationClient, UpdateStackCommand, GetTemplateCommand;
16
+ let EC2Client, ModifyVpcAttributeCommand;
17
+ let LambdaClient, UpdateFunctionConfigurationCommand;
18
+
19
+ /**
20
+ * Lazy load CloudFormation SDK
21
+ */
22
+ function loadCloudFormation() {
23
+ if (!CloudFormationClient) {
24
+ const cfModule = require('@aws-sdk/client-cloudformation');
25
+ CloudFormationClient = cfModule.CloudFormationClient;
26
+ UpdateStackCommand = cfModule.UpdateStackCommand;
27
+ GetTemplateCommand = cfModule.GetTemplateCommand;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Lazy load EC2 SDK
33
+ */
34
+ function loadEC2() {
35
+ if (!EC2Client) {
36
+ const ec2Module = require('@aws-sdk/client-ec2');
37
+ EC2Client = ec2Module.EC2Client;
38
+ ModifyVpcAttributeCommand = ec2Module.ModifyVpcAttributeCommand;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Lazy load Lambda SDK
44
+ */
45
+ function loadLambda() {
46
+ if (!LambdaClient) {
47
+ const lambdaModule = require('@aws-sdk/client-lambda');
48
+ LambdaClient = lambdaModule.LambdaClient;
49
+ UpdateFunctionConfigurationCommand = lambdaModule.UpdateFunctionConfigurationCommand;
50
+ }
51
+ }
52
+
53
+ class AWSPropertyReconciler extends IPropertyReconciler {
54
+ /**
55
+ * Resource types that support reconciliation
56
+ * @private
57
+ */
58
+ static SUPPORTED_TYPES = {
59
+ 'AWS::EC2::VPC': {
60
+ templateUpdate: true,
61
+ resourceUpdate: true,
62
+ recommendedMode: 'template',
63
+ limitations: ['Some VPC properties require resource replacement (e.g., CidrBlock)'],
64
+ },
65
+ 'AWS::EC2::Subnet': {
66
+ templateUpdate: true,
67
+ resourceUpdate: false,
68
+ recommendedMode: 'template',
69
+ limitations: ['Most Subnet properties are immutable'],
70
+ },
71
+ 'AWS::EC2::SecurityGroup': {
72
+ templateUpdate: true,
73
+ resourceUpdate: true,
74
+ recommendedMode: 'template',
75
+ limitations: ['Rule changes may cause brief connectivity interruption'],
76
+ },
77
+ 'AWS::EC2::RouteTable': {
78
+ templateUpdate: true,
79
+ resourceUpdate: false,
80
+ recommendedMode: 'template',
81
+ limitations: ['Route changes require CloudFormation update'],
82
+ },
83
+ 'AWS::RDS::DBCluster': {
84
+ templateUpdate: true,
85
+ resourceUpdate: false,
86
+ recommendedMode: 'template',
87
+ limitations: ['Many DBCluster properties require specific update windows'],
88
+ },
89
+ 'AWS::KMS::Key': {
90
+ templateUpdate: true,
91
+ resourceUpdate: false,
92
+ recommendedMode: 'template',
93
+ limitations: ['Key policy changes must be done via CloudFormation'],
94
+ },
95
+ 'AWS::Lambda::Function': {
96
+ templateUpdate: true,
97
+ resourceUpdate: true,
98
+ recommendedMode: 'template',
99
+ limitations: [
100
+ 'VpcConfig changes may take several minutes to propagate',
101
+ 'Code updates are handled separately via UpdateFunctionCode',
102
+ 'Environment variable changes may cause brief invocation errors during update',
103
+ ],
104
+ },
105
+ };
106
+
107
+ /**
108
+ * Create AWS Property Reconciler
109
+ *
110
+ * @param {Object} [config={}]
111
+ * @param {string} [config.region] - AWS region (defaults to AWS_REGION env var)
112
+ * @param {Object} [config.cloudFormationRepository] - CloudFormation repository for monitoring
113
+ */
114
+ constructor(config = {}) {
115
+ super();
116
+ this.region = config.region || process.env.AWS_REGION || 'us-east-1';
117
+ this.cfClient = null;
118
+ this.ec2Client = null;
119
+ this.lambdaClient = null;
120
+ this.cfRepo = config.cloudFormationRepository || null;
121
+ }
122
+
123
+ /**
124
+ * Get or create CloudFormation client
125
+ * @private
126
+ */
127
+ _getCFClient() {
128
+ if (!this.cfClient) {
129
+ loadCloudFormation();
130
+ this.cfClient = new CloudFormationClient({ region: this.region });
131
+ }
132
+ return this.cfClient;
133
+ }
134
+
135
+ /**
136
+ * Get or create EC2 client
137
+ * @private
138
+ */
139
+ _getEC2Client() {
140
+ if (!this.ec2Client) {
141
+ loadEC2();
142
+ this.ec2Client = new EC2Client({ region: this.region });
143
+ }
144
+ return this.ec2Client;
145
+ }
146
+
147
+ /**
148
+ * Get or create Lambda client
149
+ * @private
150
+ */
151
+ _getLambdaClient() {
152
+ if (!this.lambdaClient) {
153
+ loadLambda();
154
+ this.lambdaClient = new LambdaClient({ region: this.region });
155
+ }
156
+ return this.lambdaClient;
157
+ }
158
+
159
+ /**
160
+ * Check if a property mismatch can be auto-fixed
161
+ */
162
+ async canReconcile(mismatch) {
163
+ // Immutable properties cannot be reconciled (require replacement)
164
+ if (mismatch.requiresReplacement()) {
165
+ return false;
166
+ }
167
+
168
+ // Mutable and conditional properties can be reconciled
169
+ // Note: CONDITIONAL may require additional validation, but we treat it as reconcilable
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Reconcile a single property mismatch
175
+ */
176
+ async reconcileProperty({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
177
+ if (mode === 'template') {
178
+ return await this._reconcileViaTemplate({
179
+ stackIdentifier,
180
+ logicalId,
181
+ mismatch,
182
+ });
183
+ } else {
184
+ return await this._reconcileViaResource({
185
+ stackIdentifier,
186
+ logicalId,
187
+ mismatch,
188
+ });
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Reconcile multiple property mismatches for a resource
194
+ *
195
+ * IMPORTANT: Batches all property updates into a SINGLE UpdateStack call
196
+ * to avoid "stack is already updating" errors from CloudFormation.
197
+ *
198
+ * MONITORING: After calling UpdateStack, monitors the stack until UPDATE_COMPLETE
199
+ * or UPDATE_FAILED to ensure the update actually succeeded.
200
+ */
201
+ async reconcileMultipleProperties({
202
+ stackIdentifier,
203
+ logicalId,
204
+ physicalId,
205
+ resourceType,
206
+ mismatches,
207
+ mode = 'template',
208
+ progressMonitor = null, // Optional UpdateProgressMonitor for async tracking
209
+ }) {
210
+ // Route to appropriate reconciliation method based on mode
211
+ if (mode === 'resource') {
212
+ return await this._reconcileMultiplePropertiesViaResource({
213
+ stackIdentifier,
214
+ logicalId,
215
+ physicalId,
216
+ resourceType,
217
+ mismatches,
218
+ });
219
+ }
220
+
221
+ // Template mode (original implementation)
222
+ const results = [];
223
+ let reconciledCount = 0;
224
+ let failedCount = 0;
225
+
226
+ try {
227
+ const client = this._getCFClient();
228
+
229
+ // 1. Get current template ONCE
230
+ const getTemplateCommand = new GetTemplateCommand({
231
+ StackName: stackIdentifier.stackName,
232
+ TemplateStage: 'Original',
233
+ });
234
+
235
+ const templateResponse = await client.send(getTemplateCommand);
236
+ const template = JSON.parse(templateResponse.TemplateBody);
237
+
238
+ // 2. Apply ALL property changes to the template
239
+ for (const mismatch of mismatches) {
240
+ try {
241
+ // Navigate to the property in the template
242
+ // AWS drift detection returns paths without 'Properties.' prefix (e.g., 'VpcConfig.SubnetIds')
243
+ // But CloudFormation templates have 'Properties' section, so we need to navigate there
244
+ const pathParts = mismatch.propertyPath.split('.');
245
+ let current = template.Resources[logicalId];
246
+
247
+ // Ensure Properties section exists
248
+ if (!current.Properties) {
249
+ current.Properties = {};
250
+ }
251
+
252
+ // Start navigation at Properties level
253
+ current = current.Properties;
254
+
255
+ // Create nested objects if they don't exist
256
+ for (let i = 0; i < pathParts.length - 1; i++) {
257
+ if (!current[pathParts[i]]) {
258
+ current[pathParts[i]] = {};
259
+ }
260
+ current = current[pathParts[i]];
261
+ }
262
+
263
+ // Update the property value
264
+ const lastPart = pathParts[pathParts.length - 1];
265
+ current[lastPart] = mismatch.actualValue;
266
+
267
+ // Track as pending (will be confirmed by monitor)
268
+ results.push({
269
+ success: true,
270
+ mode: 'template',
271
+ propertyPath: mismatch.propertyPath,
272
+ oldValue: mismatch.expectedValue,
273
+ newValue: mismatch.actualValue,
274
+ message: 'Property updated in template',
275
+ });
276
+ reconciledCount++;
277
+ } catch (error) {
278
+ // Track as failed
279
+ results.push({
280
+ success: false,
281
+ mode: 'template',
282
+ propertyPath: mismatch.propertyPath,
283
+ message: `Failed to update property: ${error.message}`,
284
+ });
285
+ failedCount++;
286
+ }
287
+ }
288
+
289
+ // 3. If any properties were updated, call UpdateStack ONCE with all changes
290
+ if (reconciledCount > 0) {
291
+ const templateBody = JSON.stringify(template);
292
+ const templateSize = templateBody.length;
293
+ const TEMPLATE_SIZE_LIMIT = 51200; // CloudFormation inline template limit
294
+
295
+ // Use S3 for large templates, inline for small templates
296
+ const updateParams = {
297
+ StackName: stackIdentifier.stackName,
298
+ };
299
+
300
+ if (templateSize > TEMPLATE_SIZE_LIMIT && this.cfRepo) {
301
+ // Upload template to S3 and use TemplateURL
302
+ const templateUrl = await this.cfRepo.uploadTemplate({
303
+ stackName: stackIdentifier.stackName,
304
+ templateBody,
305
+ });
306
+ updateParams.TemplateURL = templateUrl;
307
+ } else {
308
+ // Use inline template body
309
+ updateParams.TemplateBody = templateBody;
310
+ }
311
+
312
+ // Add capabilities required for IAM resources
313
+ updateParams.Capabilities = ['CAPABILITY_NAMED_IAM'];
314
+
315
+ const updateCommand = new UpdateStackCommand(updateParams);
316
+ await client.send(updateCommand);
317
+
318
+ // 4. Monitor UpdateStack operation if CloudFormation repository available
319
+ if (this.cfRepo) {
320
+ const { UpdateProgressMonitor } = require('../../domain/services/update-progress-monitor');
321
+ const monitor = new UpdateProgressMonitor({
322
+ cloudFormationRepository: this.cfRepo,
323
+ });
324
+
325
+ const monitorResult = await monitor.monitorUpdate({
326
+ stackIdentifier,
327
+ resourceLogicalIds: [logicalId],
328
+ onProgress: (progress) => {
329
+ // Progress callback for UI updates (optional)
330
+ if (progress.status === 'FAILED') {
331
+ console.log(` ⚠ ${progress.logicalId}: Update failed - ${progress.reason}`);
332
+ }
333
+ },
334
+ });
335
+
336
+ // If monitoring detected failures, update results
337
+ if (!monitorResult.success) {
338
+ reconciledCount = 0;
339
+ failedCount = mismatches.length;
340
+ results.forEach(r => {
341
+ r.success = false;
342
+ r.message = 'CloudFormation update failed';
343
+ });
344
+
345
+ return {
346
+ reconciledCount,
347
+ failedCount,
348
+ results,
349
+ message: `Update failed: ${monitorResult.failedResources.map(f => f.reason).join(', ')}`,
350
+ };
351
+ }
352
+ }
353
+ }
354
+ } catch (error) {
355
+ // If UpdateStack fails, mark all as failed
356
+ return {
357
+ reconciledCount: 0,
358
+ failedCount: mismatches.length,
359
+ results: mismatches.map(m => ({
360
+ success: false,
361
+ mode: 'template',
362
+ propertyPath: m.propertyPath,
363
+ message: `UpdateStack failed: ${error.message}`,
364
+ })),
365
+ message: `UpdateStack failed: ${error.message}`,
366
+ };
367
+ }
368
+
369
+ return {
370
+ reconciledCount,
371
+ failedCount,
372
+ results,
373
+ message: `Reconciled ${reconciledCount} of ${mismatches.length} properties in single UpdateStack call`,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Preview property reconciliation without applying changes
379
+ */
380
+ async previewReconciliation({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
381
+ const canReconcile = await this.canReconcile(mismatch);
382
+
383
+ const warnings = [];
384
+ if (mismatch.requiresReplacement()) {
385
+ warnings.push('Property is immutable - requires resource replacement');
386
+ }
387
+
388
+ let impact = '';
389
+ if (mode === 'template') {
390
+ impact = 'Will update CloudFormation template to match actual resource state';
391
+ } else {
392
+ impact = 'Will update cloud resource to match template definition';
393
+ }
394
+
395
+ return {
396
+ canReconcile,
397
+ mode,
398
+ propertyPath: mismatch.propertyPath,
399
+ currentValue: mismatch.expectedValue,
400
+ proposedValue: mismatch.actualValue,
401
+ impact,
402
+ warnings,
403
+ };
404
+ }
405
+
406
+ /**
407
+ * Update CloudFormation template property
408
+ */
409
+ async updateTemplateProperty({ stackIdentifier, logicalId, propertyPath, newValue }) {
410
+ const client = this._getCFClient();
411
+
412
+ // Get current template
413
+ const getTemplateCommand = new GetTemplateCommand({
414
+ StackName: stackIdentifier.stackName,
415
+ TemplateStage: 'Original',
416
+ });
417
+
418
+ const templateResponse = await client.send(getTemplateCommand);
419
+ const template = JSON.parse(templateResponse.TemplateBody);
420
+
421
+ // Update property in template
422
+ const pathParts = propertyPath.split('.');
423
+ let current = template.Resources[logicalId];
424
+
425
+ for (let i = 0; i < pathParts.length - 1; i++) {
426
+ if (!current[pathParts[i]]) {
427
+ current[pathParts[i]] = {};
428
+ }
429
+ current = current[pathParts[i]];
430
+ }
431
+
432
+ const lastPart = pathParts[pathParts.length - 1];
433
+ current[lastPart] = newValue;
434
+
435
+ // Update stack with new template
436
+ const updateCommand = new UpdateStackCommand({
437
+ StackName: stackIdentifier.stackName,
438
+ TemplateBody: JSON.stringify(template),
439
+ Capabilities: ['CAPABILITY_NAMED_IAM'],
440
+ });
441
+
442
+ const updateResponse = await client.send(updateCommand);
443
+
444
+ return {
445
+ success: true,
446
+ changeSetId: updateResponse.StackId,
447
+ message: 'Template property updated successfully',
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Update cloud resource property directly
453
+ */
454
+ async updateResourceProperty({ resourceType, physicalId, region, propertyPath, newValue }) {
455
+ // Only VPC properties are supported for direct resource updates in this implementation
456
+ if (resourceType === 'AWS::EC2::VPC') {
457
+ return await this._updateVpcProperty({ physicalId, propertyPath, newValue });
458
+ }
459
+
460
+ throw new Error(`Resource type ${resourceType} updates not supported`);
461
+ }
462
+
463
+ /**
464
+ * Get reconciliation strategy for a resource type
465
+ */
466
+ async getReconciliationStrategy(resourceType) {
467
+ if (!(resourceType in AWSPropertyReconciler.SUPPORTED_TYPES)) {
468
+ throw new Error(`Resource type ${resourceType} not supported`);
469
+ }
470
+
471
+ const config = AWSPropertyReconciler.SUPPORTED_TYPES[resourceType];
472
+
473
+ return {
474
+ supportsTemplateUpdate: config.templateUpdate,
475
+ supportsResourceUpdate: config.resourceUpdate,
476
+ recommendedMode: config.recommendedMode,
477
+ limitations: config.limitations,
478
+ };
479
+ }
480
+
481
+ // ========================================
482
+ // Private Helper Methods
483
+ // ========================================
484
+
485
+ /**
486
+ * Reconcile via template update
487
+ * @private
488
+ */
489
+ async _reconcileViaTemplate({ stackIdentifier, logicalId, mismatch }) {
490
+ await this.updateTemplateProperty({
491
+ stackIdentifier,
492
+ logicalId,
493
+ propertyPath: mismatch.propertyPath,
494
+ newValue: mismatch.actualValue,
495
+ });
496
+
497
+ return {
498
+ success: true,
499
+ mode: 'template',
500
+ propertyPath: mismatch.propertyPath,
501
+ oldValue: mismatch.expectedValue,
502
+ newValue: mismatch.actualValue,
503
+ message: 'Template updated to match actual resource state',
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Reconcile via resource update
509
+ * @private
510
+ */
511
+ async _reconcileViaResource({ stackIdentifier, logicalId, mismatch }) {
512
+ // This is a simplified implementation
513
+ // In production, would need resource type detection and proper API calls
514
+
515
+ // For now, only support VPC properties
516
+ if (!mismatch.propertyPath.includes('EnableDns')) {
517
+ throw new Error('Resource property update not supported for this property');
518
+ }
519
+
520
+ // Mock resource update (in real implementation, would use EC2 API)
521
+ const client = this._getEC2Client();
522
+ const command = new ModifyVpcAttributeCommand({
523
+ VpcId: 'vpc-placeholder',
524
+ EnableDnsSupport: { Value: mismatch.expectedValue },
525
+ });
526
+
527
+ await client.send(command);
528
+
529
+ return {
530
+ success: true,
531
+ mode: 'resource',
532
+ propertyPath: mismatch.propertyPath,
533
+ oldValue: mismatch.actualValue,
534
+ newValue: mismatch.expectedValue,
535
+ message: 'Resource updated to match template definition',
536
+ };
537
+ }
538
+
539
+ /**
540
+ * Update VPC property directly
541
+ * @private
542
+ */
543
+ async _updateVpcProperty({ physicalId, propertyPath, newValue }) {
544
+ const client = this._getEC2Client();
545
+
546
+ // Map property paths to VPC attribute names
547
+ if (propertyPath === 'Properties.EnableDnsSupport') {
548
+ const command = new ModifyVpcAttributeCommand({
549
+ VpcId: physicalId,
550
+ EnableDnsSupport: { Value: newValue },
551
+ });
552
+
553
+ await client.send(command);
554
+ } else if (propertyPath === 'Properties.EnableDnsHostnames') {
555
+ const command = new ModifyVpcAttributeCommand({
556
+ VpcId: physicalId,
557
+ EnableDnsHostnames: { Value: newValue },
558
+ });
559
+
560
+ await client.send(command);
561
+ } else {
562
+ throw new Error(`Property ${propertyPath} cannot be updated directly`);
563
+ }
564
+
565
+ return {
566
+ success: true,
567
+ message: `VPC property ${propertyPath} updated successfully`,
568
+ updatedAt: new Date(),
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Reconcile multiple properties via resource update (resource mode)
574
+ *
575
+ * Domain Service - Hexagonal Architecture
576
+ * Coordinates AWS API calls to update cloud resources directly
577
+ *
578
+ * @private
579
+ */
580
+ async _reconcileMultiplePropertiesViaResource({
581
+ stackIdentifier,
582
+ logicalId,
583
+ physicalId,
584
+ resourceType,
585
+ mismatches,
586
+ }) {
587
+ // Validate resource type supports resource mode
588
+ if (resourceType !== 'AWS::Lambda::Function') {
589
+ throw new Error(`Resource mode reconciliation not supported for ${resourceType}`);
590
+ }
591
+
592
+ const results = [];
593
+ let reconciledCount = 0;
594
+ let failedCount = 0;
595
+ let skippedCount = 0;
596
+
597
+ try {
598
+ // Separate mutable from immutable properties
599
+ const mutableMismatches = [];
600
+ const mutableIndexMap = new Map(); // Track original index for mutable properties
601
+
602
+ for (let i = 0; i < mismatches.length; i++) {
603
+ const mismatch = mismatches[i];
604
+ if (mismatch.requiresReplacement()) {
605
+ skippedCount++;
606
+ } else {
607
+ mutableIndexMap.set(mismatch, i);
608
+ mutableMismatches.push(mismatch);
609
+ }
610
+ }
611
+
612
+ // If no mutable properties, return early with all marked as skipped
613
+ if (mutableMismatches.length === 0) {
614
+ const skippedResults = mismatches.map(m => ({
615
+ success: false,
616
+ mode: 'resource',
617
+ propertyPath: m.propertyPath,
618
+ message: `Skipped: Property is immutable and cannot be updated without replacement`,
619
+ }));
620
+
621
+ return {
622
+ reconciledCount: 0,
623
+ failedCount: 0,
624
+ skippedCount,
625
+ results: skippedResults,
626
+ message: `All ${mismatches.length} properties are immutable and cannot be reconciled in resource mode`,
627
+ };
628
+ }
629
+
630
+ // Route to resource-specific updater
631
+ let lambdaResults = [];
632
+ if (resourceType === 'AWS::Lambda::Function') {
633
+ const lambdaResult = await this._updateLambdaFunction({
634
+ physicalId,
635
+ mismatches: mutableMismatches,
636
+ });
637
+
638
+ reconciledCount = lambdaResult.reconciledCount;
639
+ failedCount = lambdaResult.failedCount;
640
+ lambdaResults = lambdaResult.results;
641
+ }
642
+
643
+ // Build results array in original input order
644
+ for (let i = 0; i < mismatches.length; i++) {
645
+ const mismatch = mismatches[i];
646
+ if (mismatch.requiresReplacement()) {
647
+ // Immutable - add skip result
648
+ results.push({
649
+ success: false,
650
+ mode: 'resource',
651
+ propertyPath: mismatch.propertyPath,
652
+ message: `Skipped: Property is immutable and cannot be updated without replacement`,
653
+ });
654
+ } else {
655
+ // Mutable - find corresponding result from Lambda update
656
+ const lambdaResult = lambdaResults.find(r => r.propertyPath === mismatch.propertyPath);
657
+ if (lambdaResult) {
658
+ results.push(lambdaResult);
659
+ }
660
+ }
661
+ }
662
+ } catch (error) {
663
+ // If update fails, mark all as failed
664
+ return {
665
+ reconciledCount: 0,
666
+ failedCount: mismatches.length,
667
+ skippedCount: 0,
668
+ results: mismatches.map(m => ({
669
+ success: false,
670
+ mode: 'resource',
671
+ propertyPath: m.propertyPath,
672
+ message: `Failed to update Lambda: ${error.message}`,
673
+ })),
674
+ message: `Failed to update Lambda: ${error.message}`,
675
+ };
676
+ }
677
+
678
+ // Determine appropriate message based on outcome
679
+ let message;
680
+ if (failedCount > 0 && reconciledCount === 0) {
681
+ message = `Failed to update Lambda: ${results.find(r => !r.success)?.message || 'Unknown error'}`;
682
+ } else if (failedCount > 0) {
683
+ message = `Partially updated Lambda VpcConfig (${reconciledCount} succeeded, ${failedCount} failed)`;
684
+ } else {
685
+ message = `Lambda VpcConfig updated via UpdateFunctionConfiguration (${reconciledCount} properties reconciled)`;
686
+ }
687
+
688
+ return {
689
+ reconciledCount,
690
+ failedCount,
691
+ skippedCount,
692
+ results,
693
+ message,
694
+ };
695
+ }
696
+
697
+ /**
698
+ * Update Lambda function configuration via AWS Lambda API
699
+ *
700
+ * Infrastructure Adapter
701
+ * Translates domain mismatches to AWS Lambda UpdateFunctionConfiguration call
702
+ *
703
+ * @private
704
+ */
705
+ async _updateLambdaFunction({ physicalId, mismatches }) {
706
+ const client = this._getLambdaClient();
707
+ const results = [];
708
+ let reconciledCount = 0;
709
+ let failedCount = 0;
710
+
711
+ try {
712
+ // Build VpcConfig update from mismatches
713
+ const vpcConfigUpdate = {};
714
+ const vpcConfigMismatches = mismatches.filter(m =>
715
+ m.propertyPath.startsWith('VpcConfig')
716
+ );
717
+
718
+ for (const mismatch of vpcConfigMismatches) {
719
+ // Property path format: "VpcConfig.SubnetIds" or "VpcConfig.SecurityGroupIds"
720
+ const parts = mismatch.propertyPath.split('.');
721
+ if (parts.length === 2 && parts[0] === 'VpcConfig') {
722
+ vpcConfigUpdate[parts[1]] = mismatch.expectedValue;
723
+ }
724
+ }
725
+
726
+ // If we have VpcConfig updates, call UpdateFunctionConfiguration
727
+ if (Object.keys(vpcConfigUpdate).length > 0) {
728
+ const command = new UpdateFunctionConfigurationCommand({
729
+ FunctionName: physicalId,
730
+ VpcConfig: vpcConfigUpdate,
731
+ });
732
+
733
+ await client.send(command);
734
+
735
+ // Mark all VpcConfig properties as successful
736
+ for (const mismatch of vpcConfigMismatches) {
737
+ results.push({
738
+ success: true,
739
+ mode: 'resource',
740
+ propertyPath: mismatch.propertyPath,
741
+ oldValue: mismatch.actualValue,
742
+ newValue: mismatch.expectedValue,
743
+ message: 'Lambda VpcConfig updated successfully',
744
+ });
745
+ reconciledCount++;
746
+ }
747
+ }
748
+
749
+ // Handle non-VpcConfig properties (currently unsupported)
750
+ const otherMismatches = mismatches.filter(m =>
751
+ !m.propertyPath.startsWith('VpcConfig')
752
+ );
753
+ for (const mismatch of otherMismatches) {
754
+ results.push({
755
+ success: false,
756
+ mode: 'resource',
757
+ propertyPath: mismatch.propertyPath,
758
+ message: `Property ${mismatch.propertyPath} updates not yet supported in resource mode`,
759
+ });
760
+ failedCount++;
761
+ }
762
+ } catch (error) {
763
+ // If Lambda API call fails, mark all as failed
764
+ for (const mismatch of mismatches) {
765
+ results.push({
766
+ success: false,
767
+ mode: 'resource',
768
+ propertyPath: mismatch.propertyPath,
769
+ message: `Lambda update failed: ${error.message}`,
770
+ });
771
+ failedCount++;
772
+ }
773
+ reconciledCount = 0;
774
+ }
775
+
776
+ return {
777
+ reconciledCount,
778
+ failedCount,
779
+ results,
780
+ };
781
+ }
782
+ }
783
+
784
+ module.exports = AWSPropertyReconciler;