@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,196 @@
1
+ /**
2
+ * Tests for ResourceState Value Object
3
+ */
4
+
5
+ const ResourceState = require('./resource-state');
6
+
7
+ describe('ResourceState', () => {
8
+ describe('valid states', () => {
9
+ it('should accept IN_STACK state', () => {
10
+ const state = new ResourceState('IN_STACK');
11
+
12
+ expect(state.value).toBe('IN_STACK');
13
+ });
14
+
15
+ it('should accept ORPHANED state', () => {
16
+ const state = new ResourceState('ORPHANED');
17
+
18
+ expect(state.value).toBe('ORPHANED');
19
+ });
20
+
21
+ it('should accept MISSING state', () => {
22
+ const state = new ResourceState('MISSING');
23
+
24
+ expect(state.value).toBe('MISSING');
25
+ });
26
+
27
+ it('should accept DRIFTED state', () => {
28
+ const state = new ResourceState('DRIFTED');
29
+
30
+ expect(state.value).toBe('DRIFTED');
31
+ });
32
+
33
+ it('should accept EXTERNAL state', () => {
34
+ const state = new ResourceState('EXTERNAL');
35
+
36
+ expect(state.value).toBe('EXTERNAL');
37
+ });
38
+ });
39
+
40
+ describe('invalid states', () => {
41
+ it('should reject invalid state', () => {
42
+ expect(() => {
43
+ new ResourceState('INVALID');
44
+ }).toThrow('Invalid resource state: INVALID');
45
+ });
46
+
47
+ it('should reject lowercase state', () => {
48
+ expect(() => {
49
+ new ResourceState('in_stack');
50
+ }).toThrow('Invalid resource state: in_stack');
51
+ });
52
+
53
+ it('should reject null', () => {
54
+ expect(() => {
55
+ new ResourceState(null);
56
+ }).toThrow('Resource state is required');
57
+ });
58
+
59
+ it('should reject undefined', () => {
60
+ expect(() => {
61
+ new ResourceState(undefined);
62
+ }).toThrow('Resource state is required');
63
+ });
64
+ });
65
+
66
+ describe('state checks', () => {
67
+ it('should check if resource is in stack', () => {
68
+ const state = new ResourceState('IN_STACK');
69
+
70
+ expect(state.isInStack()).toBe(true);
71
+ expect(state.isOrphaned()).toBe(false);
72
+ expect(state.isMissing()).toBe(false);
73
+ expect(state.isDrifted()).toBe(false);
74
+ expect(state.isExternal()).toBe(false);
75
+ });
76
+
77
+ it('should check if resource is orphaned', () => {
78
+ const state = new ResourceState('ORPHANED');
79
+
80
+ expect(state.isInStack()).toBe(false);
81
+ expect(state.isOrphaned()).toBe(true);
82
+ expect(state.isMissing()).toBe(false);
83
+ expect(state.isDrifted()).toBe(false);
84
+ expect(state.isExternal()).toBe(false);
85
+ });
86
+
87
+ it('should check if resource is missing', () => {
88
+ const state = new ResourceState('MISSING');
89
+
90
+ expect(state.isInStack()).toBe(false);
91
+ expect(state.isOrphaned()).toBe(false);
92
+ expect(state.isMissing()).toBe(true);
93
+ expect(state.isDrifted()).toBe(false);
94
+ expect(state.isExternal()).toBe(false);
95
+ });
96
+
97
+ it('should check if resource is drifted', () => {
98
+ const state = new ResourceState('DRIFTED');
99
+
100
+ expect(state.isInStack()).toBe(false);
101
+ expect(state.isOrphaned()).toBe(false);
102
+ expect(state.isMissing()).toBe(false);
103
+ expect(state.isDrifted()).toBe(true);
104
+ expect(state.isExternal()).toBe(false);
105
+ });
106
+
107
+ it('should check if resource is external', () => {
108
+ const state = new ResourceState('EXTERNAL');
109
+
110
+ expect(state.isInStack()).toBe(false);
111
+ expect(state.isOrphaned()).toBe(false);
112
+ expect(state.isMissing()).toBe(false);
113
+ expect(state.isDrifted()).toBe(false);
114
+ expect(state.isExternal()).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('equality', () => {
119
+ it('should be equal to same state', () => {
120
+ const state1 = new ResourceState('IN_STACK');
121
+ const state2 = new ResourceState('IN_STACK');
122
+
123
+ expect(state1.equals(state2)).toBe(true);
124
+ });
125
+
126
+ it('should not be equal to different state', () => {
127
+ const state1 = new ResourceState('IN_STACK');
128
+ const state2 = new ResourceState('ORPHANED');
129
+
130
+ expect(state1.equals(state2)).toBe(false);
131
+ });
132
+
133
+ it('should not be equal to non-ResourceState', () => {
134
+ const state = new ResourceState('IN_STACK');
135
+
136
+ expect(state.equals('IN_STACK')).toBe(false);
137
+ expect(state.equals(null)).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('toString', () => {
142
+ it('should return string representation', () => {
143
+ const state = new ResourceState('IN_STACK');
144
+
145
+ expect(state.toString()).toBe('IN_STACK');
146
+ });
147
+ });
148
+
149
+ describe('static constants', () => {
150
+ it('should provide IN_STACK constant', () => {
151
+ expect(ResourceState.IN_STACK.value).toBe('IN_STACK');
152
+ });
153
+
154
+ it('should provide ORPHANED constant', () => {
155
+ expect(ResourceState.ORPHANED.value).toBe('ORPHANED');
156
+ });
157
+
158
+ it('should provide MISSING constant', () => {
159
+ expect(ResourceState.MISSING.value).toBe('MISSING');
160
+ });
161
+
162
+ it('should provide DRIFTED constant', () => {
163
+ expect(ResourceState.DRIFTED.value).toBe('DRIFTED');
164
+ });
165
+
166
+ it('should provide EXTERNAL constant', () => {
167
+ expect(ResourceState.EXTERNAL.value).toBe('EXTERNAL');
168
+ });
169
+
170
+ it('should provide VALID_STATES array', () => {
171
+ expect(ResourceState.VALID_STATES).toEqual([
172
+ 'IN_STACK',
173
+ 'ORPHANED',
174
+ 'MISSING',
175
+ 'DRIFTED',
176
+ 'EXTERNAL',
177
+ ]);
178
+ });
179
+ });
180
+
181
+ describe('immutability', () => {
182
+ it('should not allow modification of value', () => {
183
+ const state = new ResourceState('IN_STACK');
184
+
185
+ expect(() => {
186
+ state.value = 'ORPHANED';
187
+ }).toThrow();
188
+ });
189
+
190
+ it('should be frozen', () => {
191
+ const state = new ResourceState('IN_STACK');
192
+
193
+ expect(Object.isFrozen(state)).toBe(true);
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * StackIdentifier Value Object
3
+ *
4
+ * Immutable identifier for a CloudFormation stack
5
+ * Combines stack name, region, and optional account ID
6
+ */
7
+
8
+ class StackIdentifier {
9
+ /**
10
+ * Valid AWS regions
11
+ * @private
12
+ */
13
+ static VALID_REGIONS = [
14
+ 'us-east-1',
15
+ 'us-east-2',
16
+ 'us-west-1',
17
+ 'us-west-2',
18
+ 'af-south-1',
19
+ 'ap-east-1',
20
+ 'ap-south-1',
21
+ 'ap-northeast-1',
22
+ 'ap-northeast-2',
23
+ 'ap-northeast-3',
24
+ 'ap-southeast-1',
25
+ 'ap-southeast-2',
26
+ 'ca-central-1',
27
+ 'eu-central-1',
28
+ 'eu-west-1',
29
+ 'eu-west-2',
30
+ 'eu-west-3',
31
+ 'eu-north-1',
32
+ 'eu-south-1',
33
+ 'me-south-1',
34
+ 'sa-east-1',
35
+ ];
36
+
37
+ /**
38
+ * Create a new StackIdentifier
39
+ *
40
+ * @param {Object} params
41
+ * @param {string} params.stackName - CloudFormation stack name
42
+ * @param {string} params.region - AWS region
43
+ * @param {string} [params.accountId] - AWS account ID (12 digits)
44
+ */
45
+ constructor({ stackName, region, accountId = null }) {
46
+ // Validate required fields
47
+ if (stackName === undefined || stackName === null) {
48
+ throw new Error('stackName is required');
49
+ }
50
+
51
+ if (region === undefined || region === null) {
52
+ throw new Error('region is required');
53
+ }
54
+
55
+ // Validate formats
56
+ if (typeof stackName === 'string' && stackName.trim() === '') {
57
+ throw new Error('stackName cannot be empty');
58
+ }
59
+
60
+ if (!StackIdentifier.VALID_REGIONS.includes(region)) {
61
+ throw new Error('region must be a valid AWS region');
62
+ }
63
+
64
+ if (accountId !== null && !/^\d{12}$/.test(accountId)) {
65
+ throw new Error('accountId must be a 12-digit number');
66
+ }
67
+
68
+ // Assign properties
69
+ this._stackName = stackName;
70
+ this._region = region;
71
+ this._accountId = accountId;
72
+
73
+ // Make immutable
74
+ Object.freeze(this);
75
+ }
76
+
77
+ /**
78
+ * Get stack name
79
+ * @returns {string}
80
+ */
81
+ get stackName() {
82
+ return this._stackName;
83
+ }
84
+
85
+ /**
86
+ * Prevent modification of stackName
87
+ * @throws {TypeError}
88
+ */
89
+ set stackName(value) {
90
+ throw new TypeError('Cannot modify immutable property stackName');
91
+ }
92
+
93
+ /**
94
+ * Get region
95
+ * @returns {string}
96
+ */
97
+ get region() {
98
+ return this._region;
99
+ }
100
+
101
+ /**
102
+ * Prevent modification of region
103
+ * @throws {TypeError}
104
+ */
105
+ set region(value) {
106
+ throw new TypeError('Cannot modify immutable property region');
107
+ }
108
+
109
+ /**
110
+ * Get account ID
111
+ * @returns {string|null}
112
+ */
113
+ get accountId() {
114
+ return this._accountId;
115
+ }
116
+
117
+ /**
118
+ * Prevent modification of accountId
119
+ * @throws {TypeError}
120
+ */
121
+ set accountId(value) {
122
+ throw new TypeError('Cannot modify immutable property accountId');
123
+ }
124
+
125
+ /**
126
+ * Check equality with another StackIdentifier
127
+ *
128
+ * @param {StackIdentifier} other
129
+ * @returns {boolean}
130
+ */
131
+ equals(other) {
132
+ if (!(other instanceof StackIdentifier)) {
133
+ return false;
134
+ }
135
+
136
+ return (
137
+ this.stackName === other.stackName &&
138
+ this.region === other.region &&
139
+ this.accountId === other.accountId
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Get string representation
145
+ *
146
+ * @returns {string}
147
+ */
148
+ toString() {
149
+ if (this.accountId) {
150
+ return `${this.stackName} (${this.region}, ${this.accountId})`;
151
+ }
152
+ return `${this.stackName} (${this.region})`;
153
+ }
154
+
155
+ /**
156
+ * Serialize to JSON
157
+ *
158
+ * @returns {Object}
159
+ */
160
+ toJSON() {
161
+ return {
162
+ stackName: this.stackName,
163
+ region: this.region,
164
+ accountId: this.accountId,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Create StackIdentifier from ARN
170
+ *
171
+ * @param {string} arn - CloudFormation stack ARN
172
+ * @returns {StackIdentifier}
173
+ */
174
+ static fromArn(arn) {
175
+ // arn:aws:cloudformation:region:account-id:stack/stack-name/guid
176
+ const match = arn.match(/^arn:aws:cloudformation:([^:]+):(\d{12}):stack\/([^\/]+)/);
177
+
178
+ if (!match) {
179
+ throw new Error('Invalid CloudFormation stack ARN');
180
+ }
181
+
182
+ const [, region, accountId, stackName] = match;
183
+
184
+ return new StackIdentifier({
185
+ stackName,
186
+ region,
187
+ accountId,
188
+ });
189
+ }
190
+ }
191
+
192
+ module.exports = StackIdentifier;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Tests for StackIdentifier Value Object
3
+ */
4
+
5
+ const StackIdentifier = require('./stack-identifier');
6
+
7
+ describe('StackIdentifier', () => {
8
+ describe('constructor', () => {
9
+ it('should create a valid stack identifier', () => {
10
+ const identifier = new StackIdentifier({
11
+ stackName: 'my-app-prod',
12
+ region: 'us-east-1',
13
+ accountId: '123456789012',
14
+ });
15
+
16
+ expect(identifier.stackName).toBe('my-app-prod');
17
+ expect(identifier.region).toBe('us-east-1');
18
+ expect(identifier.accountId).toBe('123456789012');
19
+ });
20
+
21
+ it('should require stackName', () => {
22
+ expect(() => {
23
+ new StackIdentifier({
24
+ region: 'us-east-1',
25
+ accountId: '123456789012',
26
+ });
27
+ }).toThrow('stackName is required');
28
+ });
29
+
30
+ it('should require region', () => {
31
+ expect(() => {
32
+ new StackIdentifier({
33
+ stackName: 'my-app-prod',
34
+ accountId: '123456789012',
35
+ });
36
+ }).toThrow('region is required');
37
+ });
38
+
39
+ it('should allow accountId to be optional', () => {
40
+ const identifier = new StackIdentifier({
41
+ stackName: 'my-app-prod',
42
+ region: 'us-east-1',
43
+ });
44
+
45
+ expect(identifier.stackName).toBe('my-app-prod');
46
+ expect(identifier.region).toBe('us-east-1');
47
+ expect(identifier.accountId).toBeNull();
48
+ });
49
+
50
+ it('should validate stackName format', () => {
51
+ expect(() => {
52
+ new StackIdentifier({
53
+ stackName: '',
54
+ region: 'us-east-1',
55
+ });
56
+ }).toThrow('stackName cannot be empty');
57
+ });
58
+
59
+ it('should validate region format', () => {
60
+ expect(() => {
61
+ new StackIdentifier({
62
+ stackName: 'my-app-prod',
63
+ region: 'invalid-region',
64
+ });
65
+ }).toThrow('region must be a valid AWS region');
66
+ });
67
+
68
+ it('should validate accountId format when provided', () => {
69
+ expect(() => {
70
+ new StackIdentifier({
71
+ stackName: 'my-app-prod',
72
+ region: 'us-east-1',
73
+ accountId: '123',
74
+ });
75
+ }).toThrow('accountId must be a 12-digit number');
76
+ });
77
+
78
+ it('should accept valid AWS regions', () => {
79
+ const validRegions = [
80
+ 'us-east-1',
81
+ 'us-east-2',
82
+ 'us-west-1',
83
+ 'us-west-2',
84
+ 'eu-west-1',
85
+ 'eu-central-1',
86
+ 'ap-southeast-1',
87
+ 'ap-northeast-1',
88
+ ];
89
+
90
+ validRegions.forEach(region => {
91
+ expect(() => {
92
+ new StackIdentifier({
93
+ stackName: 'my-app-prod',
94
+ region,
95
+ });
96
+ }).not.toThrow();
97
+ });
98
+ });
99
+ });
100
+
101
+ describe('equals', () => {
102
+ it('should return true for identical identifiers', () => {
103
+ const id1 = new StackIdentifier({
104
+ stackName: 'my-app-prod',
105
+ region: 'us-east-1',
106
+ accountId: '123456789012',
107
+ });
108
+
109
+ const id2 = new StackIdentifier({
110
+ stackName: 'my-app-prod',
111
+ region: 'us-east-1',
112
+ accountId: '123456789012',
113
+ });
114
+
115
+ expect(id1.equals(id2)).toBe(true);
116
+ });
117
+
118
+ it('should return false for different stack names', () => {
119
+ const id1 = new StackIdentifier({
120
+ stackName: 'my-app-prod',
121
+ region: 'us-east-1',
122
+ });
123
+
124
+ const id2 = new StackIdentifier({
125
+ stackName: 'my-app-dev',
126
+ region: 'us-east-1',
127
+ });
128
+
129
+ expect(id1.equals(id2)).toBe(false);
130
+ });
131
+
132
+ it('should return false for different regions', () => {
133
+ const id1 = new StackIdentifier({
134
+ stackName: 'my-app-prod',
135
+ region: 'us-east-1',
136
+ });
137
+
138
+ const id2 = new StackIdentifier({
139
+ stackName: 'my-app-prod',
140
+ region: 'us-west-2',
141
+ });
142
+
143
+ expect(id1.equals(id2)).toBe(false);
144
+ });
145
+
146
+ it('should return false for different account IDs', () => {
147
+ const id1 = new StackIdentifier({
148
+ stackName: 'my-app-prod',
149
+ region: 'us-east-1',
150
+ accountId: '123456789012',
151
+ });
152
+
153
+ const id2 = new StackIdentifier({
154
+ stackName: 'my-app-prod',
155
+ region: 'us-east-1',
156
+ accountId: '987654321098',
157
+ });
158
+
159
+ expect(id1.equals(id2)).toBe(false);
160
+ });
161
+
162
+ it('should handle null accountId comparison', () => {
163
+ const id1 = new StackIdentifier({
164
+ stackName: 'my-app-prod',
165
+ region: 'us-east-1',
166
+ });
167
+
168
+ const id2 = new StackIdentifier({
169
+ stackName: 'my-app-prod',
170
+ region: 'us-east-1',
171
+ });
172
+
173
+ expect(id1.equals(id2)).toBe(true);
174
+ });
175
+ });
176
+
177
+ describe('toString', () => {
178
+ it('should return string representation with account ID', () => {
179
+ const identifier = new StackIdentifier({
180
+ stackName: 'my-app-prod',
181
+ region: 'us-east-1',
182
+ accountId: '123456789012',
183
+ });
184
+
185
+ expect(identifier.toString()).toBe('my-app-prod (us-east-1, 123456789012)');
186
+ });
187
+
188
+ it('should return string representation without account ID', () => {
189
+ const identifier = new StackIdentifier({
190
+ stackName: 'my-app-prod',
191
+ region: 'us-east-1',
192
+ });
193
+
194
+ expect(identifier.toString()).toBe('my-app-prod (us-east-1)');
195
+ });
196
+ });
197
+
198
+ describe('toJSON', () => {
199
+ it('should serialize to JSON with account ID', () => {
200
+ const identifier = new StackIdentifier({
201
+ stackName: 'my-app-prod',
202
+ region: 'us-east-1',
203
+ accountId: '123456789012',
204
+ });
205
+
206
+ expect(identifier.toJSON()).toEqual({
207
+ stackName: 'my-app-prod',
208
+ region: 'us-east-1',
209
+ accountId: '123456789012',
210
+ });
211
+ });
212
+
213
+ it('should serialize to JSON without account ID', () => {
214
+ const identifier = new StackIdentifier({
215
+ stackName: 'my-app-prod',
216
+ region: 'us-east-1',
217
+ });
218
+
219
+ expect(identifier.toJSON()).toEqual({
220
+ stackName: 'my-app-prod',
221
+ region: 'us-east-1',
222
+ accountId: null,
223
+ });
224
+ });
225
+ });
226
+
227
+ describe('immutability', () => {
228
+ it('should not allow modification of stackName', () => {
229
+ const identifier = new StackIdentifier({
230
+ stackName: 'my-app-prod',
231
+ region: 'us-east-1',
232
+ });
233
+
234
+ expect(() => {
235
+ identifier.stackName = 'modified';
236
+ }).toThrow();
237
+ });
238
+
239
+ it('should not allow modification of region', () => {
240
+ const identifier = new StackIdentifier({
241
+ stackName: 'my-app-prod',
242
+ region: 'us-east-1',
243
+ });
244
+
245
+ expect(() => {
246
+ identifier.region = 'us-west-2';
247
+ }).toThrow();
248
+ });
249
+
250
+ it('should not allow modification of accountId', () => {
251
+ const identifier = new StackIdentifier({
252
+ stackName: 'my-app-prod',
253
+ region: 'us-east-1',
254
+ accountId: '123456789012',
255
+ });
256
+
257
+ expect(() => {
258
+ identifier.accountId = '987654321098';
259
+ }).toThrow();
260
+ });
261
+ });
262
+ });