@friggframework/devtools 2.0.0-next.45 → 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 -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,687 @@
1
+ /**
2
+ * CloudFormation Template Generation Tests
3
+ *
4
+ * These tests validate that the infrastructure builders generate
5
+ * valid, consistent CloudFormation templates.
6
+ */
7
+
8
+ const { composeServerlessDefinition } = require('../infrastructure-composer');
9
+
10
+ describe('CloudFormation Template Generation', () => {
11
+ // Mock AWS region for tests (prevents actual AWS calls)
12
+ beforeAll(() => {
13
+ process.env.AWS_REGION = 'us-east-1';
14
+ // Note: Not setting FRIGG_SKIP_AWS_DISCOVERY - we want builders to execute
15
+ // Resource discovery will return empty results which is fine for testing
16
+ });
17
+
18
+ afterAll(() => {
19
+ delete process.env.AWS_REGION;
20
+ });
21
+
22
+ describe('Basic Structure', () => {
23
+ it('should generate template with required top-level properties', async () => {
24
+ const appDefinition = {
25
+ name: 'test-app',
26
+ provider: 'aws',
27
+ region: 'us-east-1',
28
+ integrations: []
29
+ };
30
+
31
+ const result = await composeServerlessDefinition(appDefinition);
32
+
33
+ expect(result).toBeDefined();
34
+ expect(result.provider).toBeDefined();
35
+ expect(result.service).toBe('test-app');
36
+ });
37
+
38
+ it('should include functions section', async () => {
39
+ const appDefinition = {
40
+ name: 'test-app',
41
+ integrations: [
42
+ { Definition: { name: 'slack' } }
43
+ ]
44
+ };
45
+
46
+ const result = await composeServerlessDefinition(appDefinition);
47
+
48
+ expect(result.functions).toBeDefined();
49
+ expect(typeof result.functions).toBe('object');
50
+ });
51
+
52
+ it('should include resources section', async () => {
53
+ const appDefinition = {
54
+ name: 'test-app',
55
+ integrations: [
56
+ { Definition: { name: 'slack' } }
57
+ ]
58
+ };
59
+
60
+ const result = await composeServerlessDefinition(appDefinition);
61
+
62
+ expect(result.resources).toBeDefined();
63
+ expect(result.resources.Resources).toBeDefined();
64
+ expect(result.provider.iamRoleStatements).toBeDefined();
65
+ });
66
+ });
67
+
68
+ describe('Resource Reference Validation', () => {
69
+ it('should have valid Ref references', async () => {
70
+ const appDefinition = {
71
+ name: 'test-app',
72
+ integrations: [
73
+ { Definition: { name: 'slack' } }
74
+ ]
75
+ };
76
+
77
+ const result = await composeServerlessDefinition(appDefinition);
78
+
79
+ // Extract all resources and layers
80
+ const resources = result.resources.Resources || {};
81
+ const layers = result.layers || {};
82
+ const allIds = [...Object.keys(resources), ...Object.keys(layers).map(k => `${k.charAt(0).toUpperCase()}${k.slice(1)}LambdaLayer`)];
83
+
84
+ // Find all Ref references
85
+ const refs = findAllRefs(result);
86
+
87
+ // Framework-generated resources (created by Serverless Framework, not our builders)
88
+ const frameworkResources = ['HttpApi', 'HttpApiLogGroup'];
89
+
90
+ // Verify all Refs point to existing resources, layers, or framework resources
91
+ refs.forEach(ref => {
92
+ if (!frameworkResources.includes(ref)) {
93
+ expect(allIds).toContain(ref);
94
+ }
95
+ });
96
+ });
97
+
98
+ it('should have valid Fn::GetAtt references', async () => {
99
+ const appDefinition = {
100
+ name: 'test-app',
101
+ integrations: [
102
+ { Definition: { name: 'slack' } }
103
+ ]
104
+ };
105
+
106
+ const result = await composeServerlessDefinition(appDefinition);
107
+
108
+ const resources = result.resources.Resources || {};
109
+ const resourceIds = Object.keys(resources);
110
+
111
+ // Find all GetAtt references
112
+ const getAtts = findAllGetAtts(result);
113
+
114
+ // Verify all GetAtt references point to existing resources
115
+ getAtts.forEach(([resourceId]) => {
116
+ expect(resourceIds).toContain(resourceId);
117
+ });
118
+ });
119
+
120
+ it('should not have circular references', async () => {
121
+ const appDefinition = {
122
+ name: 'test-app',
123
+ integrations: [
124
+ { Definition: { name: 'slack' } }
125
+ ],
126
+ vpc: { enable: true },
127
+ encryption: { fieldLevelEncryptionMethod: 'kms' }
128
+ };
129
+
130
+ const result = await composeServerlessDefinition(appDefinition);
131
+
132
+ const resources = result.resources.Resources || {};
133
+
134
+ // Build dependency graph
135
+ const graph = buildDependencyGraph(resources);
136
+
137
+ // Check for circular dependencies
138
+ const cycles = detectCycles(graph);
139
+
140
+ expect(cycles).toEqual([]);
141
+ });
142
+ });
143
+
144
+ describe('Integration Resources', () => {
145
+ it('should create queue for each integration', async () => {
146
+ const appDefinition = {
147
+ name: 'test-app',
148
+ integrations: [
149
+ { Definition: { name: 'slack' } },
150
+ { Definition: { name: 'hubspot' } }
151
+ ]
152
+ };
153
+
154
+ const result = await composeServerlessDefinition(appDefinition);
155
+
156
+ expect(result.resources.Resources.SlackQueue).toBeDefined();
157
+ expect(result.resources.Resources.SlackQueue.Type).toBe('AWS::SQS::Queue');
158
+ expect(result.resources.Resources.HubspotQueue).toBeDefined();
159
+ expect(result.resources.Resources.HubspotQueue.Type).toBe('AWS::SQS::Queue');
160
+ });
161
+
162
+ it('should create InternalErrorQueue as DLQ', async () => {
163
+ const appDefinition = {
164
+ name: 'test-app',
165
+ integrations: [
166
+ { Definition: { name: 'slack' } }
167
+ ]
168
+ };
169
+
170
+ const result = await composeServerlessDefinition(appDefinition);
171
+
172
+ expect(result.resources.Resources.InternalErrorQueue).toBeDefined();
173
+ expect(result.resources.Resources.InternalErrorQueue.Type).toBe('AWS::SQS::Queue');
174
+ });
175
+
176
+ it('should configure redrive policy to InternalErrorQueue', async () => {
177
+ const appDefinition = {
178
+ name: 'test-app',
179
+ integrations: [
180
+ { Definition: { name: 'slack' } }
181
+ ]
182
+ };
183
+
184
+ const result = await composeServerlessDefinition(appDefinition);
185
+
186
+ const slackQueue = result.resources.Resources.SlackQueue;
187
+ expect(slackQueue.Properties.RedrivePolicy).toBeDefined();
188
+ expect(slackQueue.Properties.RedrivePolicy.deadLetterTargetArn).toEqual({
189
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn']
190
+ });
191
+ });
192
+
193
+ it('should create Lambda functions for each integration', async () => {
194
+ const appDefinition = {
195
+ name: 'test-app',
196
+ integrations: [
197
+ { Definition: { name: 'slack' } }
198
+ ]
199
+ };
200
+
201
+ const result = await composeServerlessDefinition(appDefinition);
202
+
203
+ // HTTP handler
204
+ expect(result.functions.slack).toBeDefined();
205
+ expect(result.functions.slack.handler).toContain('integration-defined-routers');
206
+
207
+ // Queue worker
208
+ expect(result.functions.slackQueueWorker).toBeDefined();
209
+ expect(result.functions.slackQueueWorker.handler).toContain('integration-defined-workers');
210
+ });
211
+
212
+ it('should create webhook handler when enabled', async () => {
213
+ const appDefinition = {
214
+ name: 'test-app',
215
+ integrations: [
216
+ {
217
+ Definition: {
218
+ name: 'hubspot',
219
+ webhooks: true
220
+ }
221
+ }
222
+ ]
223
+ };
224
+
225
+ const result = await composeServerlessDefinition(appDefinition);
226
+
227
+ expect(result.functions.hubspotWebhook).toBeDefined();
228
+ expect(result.functions.hubspotWebhook.handler).toContain('integration-webhook-routers');
229
+ });
230
+
231
+ it('should add queue worker SQS event trigger', async () => {
232
+ const appDefinition = {
233
+ name: 'test-app',
234
+ integrations: [
235
+ { Definition: { name: 'slack' } }
236
+ ]
237
+ };
238
+
239
+ const result = await composeServerlessDefinition(appDefinition);
240
+
241
+ const queueWorker = result.functions.slackQueueWorker;
242
+ expect(queueWorker.events).toBeDefined();
243
+ expect(queueWorker.events[0].sqs).toBeDefined();
244
+ expect(queueWorker.events[0].sqs.arn).toEqual({
245
+ 'Fn::GetAtt': ['SlackQueue', 'Arn']
246
+ });
247
+ });
248
+ });
249
+
250
+ describe('VPC Resources', () => {
251
+ it('should create VPC resources when enabled', async () => {
252
+ const appDefinition = {
253
+ name: 'test-app',
254
+ vpc: { enable: true },
255
+ integrations: []
256
+ };
257
+
258
+ const result = await composeServerlessDefinition(appDefinition);
259
+
260
+ expect(result.resources.Resources.FriggVPC).toBeDefined();
261
+ expect(result.resources.Resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
262
+ });
263
+
264
+ it('should create subnets in VPC', async () => {
265
+ const appDefinition = {
266
+ name: 'test-app',
267
+ vpc: { enable: true },
268
+ integrations: []
269
+ };
270
+
271
+ const result = await composeServerlessDefinition(appDefinition);
272
+
273
+ expect(result.resources.Resources.FriggPrivateSubnet1).toBeDefined();
274
+ expect(result.resources.Resources.FriggPrivateSubnet2).toBeDefined();
275
+ });
276
+
277
+ it('should create security group in VPC', async () => {
278
+ const appDefinition = {
279
+ name: 'test-app',
280
+ vpc: { enable: true },
281
+ integrations: []
282
+ };
283
+
284
+ const result = await composeServerlessDefinition(appDefinition);
285
+
286
+ expect(result.resources.Resources.FriggLambdaSecurityGroup).toBeDefined();
287
+ expect(result.resources.Resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
288
+ });
289
+
290
+ it('should configure Lambda functions with VPC', async () => {
291
+ const appDefinition = {
292
+ name: 'test-app',
293
+ vpc: { enable: true },
294
+ integrations: [
295
+ { Definition: { name: 'slack' } }
296
+ ]
297
+ };
298
+
299
+ const result = await composeServerlessDefinition(appDefinition);
300
+
301
+ expect(result.provider.vpc).toBeDefined();
302
+ expect(result.provider.vpc.subnetIds).toBeDefined();
303
+ expect(result.provider.vpc.securityGroupIds).toBeDefined();
304
+ });
305
+
306
+ it('should not create VPC resources when disabled', async () => {
307
+ const appDefinition = {
308
+ name: 'test-app',
309
+ integrations: []
310
+ };
311
+
312
+ const result = await composeServerlessDefinition(appDefinition);
313
+
314
+ expect(result.resources.Resources.FriggVPC).toBeUndefined();
315
+ });
316
+ });
317
+
318
+ describe('KMS Configuration', () => {
319
+ it('should configure KMS ARN when encryption enabled', async () => {
320
+ const appDefinition = {
321
+ name: 'test-app',
322
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
323
+ integrations: []
324
+ };
325
+
326
+ const result = await composeServerlessDefinition(appDefinition);
327
+
328
+ // KMS uses discovered keys, not CloudFormation resources
329
+ // Verify environment variable is set
330
+ expect(result.provider.environment.KMS_KEY_ARN).toBeDefined();
331
+ });
332
+
333
+ it('should grant KMS permissions when encryption enabled', async () => {
334
+ const appDefinition = {
335
+ name: 'test-app',
336
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
337
+ integrations: []
338
+ };
339
+
340
+ const result = await composeServerlessDefinition(appDefinition);
341
+
342
+ const kmsStatements = result.provider.iamRoleStatements.filter(stmt =>
343
+ stmt.Action && stmt.Action.some(action => action.startsWith('kms:'))
344
+ );
345
+
346
+ expect(kmsStatements.length).toBeGreaterThan(0);
347
+ });
348
+
349
+ it('should use external KMS key when configured', async () => {
350
+ const appDefinition = {
351
+ name: 'test-app',
352
+ encryption: {
353
+ fieldLevelEncryptionMethod: 'kms',
354
+ ownership: { kmsKey: 'external' },
355
+ external: { kmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/external' }
356
+ },
357
+ integrations: []
358
+ };
359
+
360
+ const result = await composeServerlessDefinition(appDefinition);
361
+
362
+ // Should use provided external ARN (may be wrapped in CloudFormation function)
363
+ const kmsArn = result.provider.environment.KMS_KEY_ARN;
364
+ if (typeof kmsArn === 'string') {
365
+ expect(kmsArn).toBe('arn:aws:kms:us-east-1:123456789012:key/external');
366
+ } else {
367
+ // Intrinsic function reference
368
+ expect(kmsArn).toBeDefined();
369
+ }
370
+ });
371
+ });
372
+
373
+ describe('Migration Resources', () => {
374
+ it('should create migration queue for PostgreSQL', async () => {
375
+ const appDefinition = {
376
+ name: 'test-app',
377
+ database: {
378
+ type: 'postgresql',
379
+ aurora: { enable: true }
380
+ },
381
+ integrations: []
382
+ };
383
+
384
+ const result = await composeServerlessDefinition(appDefinition);
385
+
386
+ expect(result.resources.Resources.DbMigrationQueue).toBeDefined();
387
+ expect(result.resources.Resources.DbMigrationQueue.Type).toBe('AWS::SQS::Queue');
388
+ });
389
+
390
+ it('should create S3 bucket for migration status', async () => {
391
+ const appDefinition = {
392
+ name: 'test-app',
393
+ database: {
394
+ type: 'postgresql',
395
+ aurora: { enable: true }
396
+ },
397
+ integrations: []
398
+ };
399
+
400
+ const result = await composeServerlessDefinition(appDefinition);
401
+
402
+ expect(result.resources.Resources.FriggMigrationStatusBucket).toBeDefined();
403
+ expect(result.resources.Resources.FriggMigrationStatusBucket.Type).toBe('AWS::S3::Bucket');
404
+ });
405
+
406
+ it('should set DeletionPolicy=Retain on migration bucket', async () => {
407
+ const appDefinition = {
408
+ name: 'test-app',
409
+ database: {
410
+ type: 'postgresql',
411
+ aurora: { enable: true }
412
+ },
413
+ integrations: []
414
+ };
415
+
416
+ const result = await composeServerlessDefinition(appDefinition);
417
+
418
+ expect(result.resources.Resources.FriggMigrationStatusBucket.DeletionPolicy).toBe('Retain');
419
+ });
420
+ });
421
+
422
+ describe('Resource Consistency', () => {
423
+ it('should have no duplicate resource logical IDs', async () => {
424
+ const appDefinition = {
425
+ name: 'test-app',
426
+ integrations: [
427
+ { Definition: { name: 'slack' } },
428
+ { Definition: { name: 'hubspot' } }
429
+ ],
430
+ vpc: { enable: true },
431
+ encryption: { useDefaultKMSForFieldLevelEncryption: true },
432
+ database: {
433
+ type: 'postgresql',
434
+ aurora: { enable: true }
435
+ }
436
+ };
437
+
438
+ const result = await composeServerlessDefinition(appDefinition);
439
+
440
+ const resources = result.resources.Resources || {};
441
+ const resourceIds = Object.keys(resources);
442
+ const uniqueIds = new Set(resourceIds);
443
+
444
+ expect(resourceIds.length).toBe(uniqueIds.size);
445
+ });
446
+
447
+ it('should have consistent resource naming', async () => {
448
+ const appDefinition = {
449
+ name: 'test-app',
450
+ integrations: [
451
+ { Definition: { name: 'my-integration' } }
452
+ ]
453
+ };
454
+
455
+ const result = await composeServerlessDefinition(appDefinition);
456
+
457
+ // Queue name should be capitalized (first letter only)
458
+ expect(result.resources.Resources['My-integrationQueue']).toBeDefined();
459
+
460
+ // Function name should match integration name
461
+ expect(result.functions['my-integration']).toBeDefined();
462
+ expect(result.functions['my-integrationQueueWorker']).toBeDefined();
463
+ });
464
+
465
+ it('should set appropriate timeouts for queue workers', async () => {
466
+ const appDefinition = {
467
+ name: 'test-app',
468
+ integrations: [
469
+ { Definition: { name: 'slack' } }
470
+ ]
471
+ };
472
+
473
+ const result = await composeServerlessDefinition(appDefinition);
474
+
475
+ const queueWorker = result.functions.slackQueueWorker;
476
+ expect(queueWorker.timeout).toBe(900); // 15 minutes (Lambda max)
477
+ });
478
+
479
+ it('should configure appropriate queue visibility timeout', async () => {
480
+ const appDefinition = {
481
+ name: 'test-app',
482
+ integrations: [
483
+ { Definition: { name: 'slack' } }
484
+ ]
485
+ };
486
+
487
+ const result = await composeServerlessDefinition(appDefinition);
488
+
489
+ const queue = result.resources.Resources.SlackQueue;
490
+ expect(queue.Properties.VisibilityTimeout).toBe(1800); // 30 minutes (2x Lambda timeout)
491
+ });
492
+ });
493
+
494
+ describe('Environment Variables', () => {
495
+ it('should add queue URLs to environment', async () => {
496
+ const appDefinition = {
497
+ name: 'test-app',
498
+ integrations: [
499
+ { Definition: { name: 'slack' } }
500
+ ]
501
+ };
502
+
503
+ const result = await composeServerlessDefinition(appDefinition);
504
+
505
+ expect(result.provider.environment.SLACK_QUEUE_URL).toEqual({
506
+ Ref: 'SlackQueue'
507
+ });
508
+ });
509
+
510
+ it('should add KMS key ARN when encryption enabled', async () => {
511
+ const appDefinition = {
512
+ name: 'test-app',
513
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
514
+ integrations: []
515
+ };
516
+
517
+ const result = await composeServerlessDefinition(appDefinition);
518
+
519
+ expect(result.provider.environment.KMS_KEY_ARN).toBeDefined();
520
+ });
521
+
522
+ it('should add migration environment variables for PostgreSQL', async () => {
523
+ const appDefinition = {
524
+ name: 'test-app',
525
+ database: {
526
+ type: 'postgresql',
527
+ aurora: { enable: true }
528
+ },
529
+ integrations: []
530
+ };
531
+
532
+ const result = await composeServerlessDefinition(appDefinition);
533
+
534
+ expect(result.provider.environment.DB_MIGRATION_QUEUE_URL).toBeDefined();
535
+ expect(result.provider.environment.MIGRATION_STATUS_BUCKET).toBeDefined();
536
+ });
537
+ });
538
+
539
+ describe('IAM Permissions', () => {
540
+ it('should grant SQS permissions for integration queues', async () => {
541
+ const appDefinition = {
542
+ name: 'test-app',
543
+ integrations: [
544
+ { Definition: { name: 'slack' } }
545
+ ]
546
+ };
547
+
548
+ const result = await composeServerlessDefinition(appDefinition);
549
+
550
+ const sqsStatements = result.provider.iamRoleStatements.filter(stmt =>
551
+ stmt.Action && stmt.Action.some(action => action.startsWith('sqs:'))
552
+ );
553
+
554
+ expect(sqsStatements.length).toBeGreaterThan(0);
555
+ });
556
+
557
+ it('should grant S3 permissions for migration bucket', async () => {
558
+ const appDefinition = {
559
+ name: 'test-app',
560
+ database: {
561
+ type: 'postgresql',
562
+ aurora: { enable: true }
563
+ },
564
+ integrations: []
565
+ };
566
+
567
+ const result = await composeServerlessDefinition(appDefinition);
568
+
569
+ const s3Statements = result.provider.iamRoleStatements.filter(stmt =>
570
+ stmt.Action && stmt.Action.some(action => action.startsWith('s3:'))
571
+ );
572
+
573
+ expect(s3Statements.length).toBeGreaterThan(0);
574
+ });
575
+ });
576
+
577
+ describe('Ownership-Based Decisions', () => {
578
+ it('should use existing stack resources when discovered', async () => {
579
+ // This test would need actual discovery data
580
+ // Skipping for now - integration tests would cover this
581
+ expect(true).toBe(true);
582
+ });
583
+
584
+ it('should use external resources when configured', async () => {
585
+ const appDefinition = {
586
+ name: 'test-app',
587
+ integrations: [
588
+ {
589
+ Definition: { name: 'slack' },
590
+ ownership: { queue: 'external' },
591
+ queue: { url: 'https://sqs.us-east-1.amazonaws.com/123/my-queue' }
592
+ }
593
+ ]
594
+ };
595
+
596
+ const result = await composeServerlessDefinition(appDefinition);
597
+
598
+ // Should not create queue resource
599
+ expect(result.resources.Resources.SlackQueue).toBeUndefined();
600
+
601
+ // Should use external queue URL in environment
602
+ expect(result.provider.environment.SLACK_QUEUE_URL).toBe('https://sqs.us-east-1.amazonaws.com/123/my-queue');
603
+ });
604
+ });
605
+ });
606
+
607
+ // Helper functions
608
+ function findAllRefs(obj, refs = []) {
609
+ if (typeof obj !== 'object' || obj === null) return refs;
610
+
611
+ if (obj.Ref && typeof obj.Ref === 'string') {
612
+ refs.push(obj.Ref);
613
+ }
614
+
615
+ Object.values(obj).forEach(value => findAllRefs(value, refs));
616
+ return refs;
617
+ }
618
+
619
+ function findAllGetAtts(obj, getAtts = []) {
620
+ if (typeof obj !== 'object' || obj === null) return getAtts;
621
+
622
+ if (obj['Fn::GetAtt'] && Array.isArray(obj['Fn::GetAtt'])) {
623
+ getAtts.push(obj['Fn::GetAtt']);
624
+ }
625
+
626
+ Object.values(obj).forEach(value => findAllGetAtts(value, getAtts));
627
+ return getAtts;
628
+ }
629
+
630
+ function buildDependencyGraph(resources) {
631
+ const graph = {};
632
+
633
+ Object.keys(resources).forEach(resourceId => {
634
+ const deps = new Set();
635
+ const resource = resources[resourceId];
636
+
637
+ // Find Ref dependencies
638
+ findAllRefs(resource).forEach(ref => {
639
+ if (ref !== resourceId && resources[ref]) {
640
+ deps.add(ref);
641
+ }
642
+ });
643
+
644
+ // Find GetAtt dependencies
645
+ findAllGetAtts(resource).forEach(([ref]) => {
646
+ if (ref !== resourceId && resources[ref]) {
647
+ deps.add(ref);
648
+ }
649
+ });
650
+
651
+ graph[resourceId] = Array.from(deps);
652
+ });
653
+
654
+ return graph;
655
+ }
656
+
657
+ function detectCycles(graph) {
658
+ const visited = new Set();
659
+ const recursionStack = new Set();
660
+ const cycles = [];
661
+
662
+ function dfs(node, path = []) {
663
+ if (recursionStack.has(node)) {
664
+ cycles.push([...path, node]);
665
+ return;
666
+ }
667
+
668
+ if (visited.has(node)) return;
669
+
670
+ visited.add(node);
671
+ recursionStack.add(node);
672
+ path.push(node);
673
+
674
+ const neighbors = graph[node] || [];
675
+ neighbors.forEach(neighbor => dfs(neighbor, [...path]));
676
+
677
+ recursionStack.delete(node);
678
+ }
679
+
680
+ Object.keys(graph).forEach(node => {
681
+ if (!visited.has(node)) {
682
+ dfs(node);
683
+ }
684
+ });
685
+
686
+ return cycles;
687
+ }
@@ -1,6 +1,6 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
- const { composeServerlessDefinition } = require('./serverless-template');
3
+ const { composeServerlessDefinition } = require('./infrastructure-composer');
4
4
  const { findNearestBackendPackageJson } = require('@friggframework/core');
5
5
 
6
6
  async function createFriggInfrastructure() {