@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,170 @@
1
+ /**
2
+ * Integration Resource Resolver
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for resolving ownership decisions for integration infrastructure:
7
+ * - SQS queues per integration
8
+ *
9
+ * Follows the ownership-based architecture pattern:
10
+ * - STACK: Create/manage resources in CloudFormation stack
11
+ * - EXTERNAL: Use existing resources by physical ID
12
+ * - AUTO: Intelligent decision based on discovery
13
+ */
14
+
15
+ const BaseResourceResolver = require('../shared/base-resolver');
16
+ const { ResourceOwnership } = require('../shared/types/resource-ownership');
17
+
18
+ class IntegrationResourceResolver extends BaseResourceResolver {
19
+ /**
20
+ * Resolve ownership for all integration resources
21
+ * @param {Object} appDefinition - Application definition
22
+ * @param {Object} discovery - Structured discovery result
23
+ * @returns {Object} Ownership decisions for all integration resources
24
+ */
25
+ resolveAll(appDefinition, discovery) {
26
+ const integrations = appDefinition.integrations || [];
27
+ const decisions = {
28
+ // Shared resources used by all integrations
29
+ internalErrorQueue: this.resolveInternalErrorQueue(appDefinition, discovery),
30
+ // Per-integration queues
31
+ integrations: {},
32
+ };
33
+
34
+ // Resolve ownership for each integration's SQS queue
35
+ integrations.forEach(integration => {
36
+ const integrationName = integration.Definition?.name;
37
+ if (!integrationName) {
38
+ return; // Skip invalid integrations
39
+ }
40
+
41
+ decisions.integrations[integrationName] = {
42
+ queue: this.resolveQueue(integrationName, appDefinition, discovery),
43
+ };
44
+ });
45
+
46
+ return decisions;
47
+ }
48
+
49
+ /**
50
+ * Resolve ownership for the shared InternalErrorQueue (dead letter queue)
51
+ * @param {Object} appDefinition - Application definition
52
+ * @param {Object} discovery - Structured discovery result
53
+ * @returns {Object} Ownership decision for InternalErrorQueue
54
+ */
55
+ resolveInternalErrorQueue(appDefinition, discovery) {
56
+ const userIntent = appDefinition.integrations?.ownership?.internalErrorQueue || ResourceOwnership.AUTO;
57
+ const queueLogicalId = 'InternalErrorQueue';
58
+ const inStack = this.isInStack(queueLogicalId, discovery);
59
+
60
+ // STACK: User wants this in the CloudFormation stack
61
+ if (userIntent === ResourceOwnership.STACK) {
62
+ const stackResource = inStack ? this.findInStack(queueLogicalId, discovery) : null;
63
+ return this.createStackDecision(
64
+ stackResource?.physicalId || null,
65
+ inStack
66
+ ? `Found ${queueLogicalId} in CloudFormation stack`
67
+ : `Will create ${queueLogicalId} in stack`
68
+ );
69
+ }
70
+
71
+ // EXTERNAL: User wants to use an existing queue
72
+ if (userIntent === ResourceOwnership.EXTERNAL) {
73
+ const queueArn = appDefinition.integrations?.internalErrorQueue?.arn;
74
+ if (!queueArn) {
75
+ throw new Error(
76
+ 'InternalErrorQueue configured with ownership=external but integrations.internalErrorQueue.arn not provided'
77
+ );
78
+ }
79
+ return this.createExternalDecision(
80
+ queueArn,
81
+ `Using external InternalErrorQueue ARN: ${queueArn}`
82
+ );
83
+ }
84
+
85
+ // AUTO: Intelligent decision based on discovery
86
+ if (inStack) {
87
+ const stackResource = this.findInStack(queueLogicalId, discovery);
88
+ return this.createStackDecision(
89
+ stackResource.physicalId,
90
+ `Found ${queueLogicalId} in CloudFormation stack - will include definition (idempotent)`
91
+ );
92
+ }
93
+
94
+ // Default: Create in stack
95
+ return this.createStackDecision(
96
+ null,
97
+ `No existing ${queueLogicalId} found - will create in stack`
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Resolve ownership for an integration's SQS queue
103
+ * @param {string} integrationName - Name of the integration (e.g., 'slack')
104
+ * @param {Object} appDefinition - Application definition
105
+ * @param {Object} discovery - Structured discovery result
106
+ * @returns {Object} Ownership decision for the queue
107
+ */
108
+ resolveQueue(integrationName, appDefinition, discovery) {
109
+ // Check for explicit ownership configuration
110
+ const integration = appDefinition.integrations?.find(
111
+ i => i.Definition?.name === integrationName
112
+ );
113
+ const userIntent = integration?.ownership?.queue || ResourceOwnership.AUTO;
114
+
115
+ // Queue logical ID follows pattern: ${IntegrationName}Queue (e.g., SlackQueue)
116
+ const queueLogicalId = `${this.capitalizeFirst(integrationName)}Queue`;
117
+ const inStack = this.isInStack(queueLogicalId, discovery);
118
+
119
+ // STACK: User wants this in the CloudFormation stack
120
+ if (userIntent === ResourceOwnership.STACK) {
121
+ const stackResource = inStack ? this.findInStack(queueLogicalId, discovery) : null;
122
+ return this.createStackDecision(
123
+ stackResource?.physicalId || null,
124
+ inStack
125
+ ? `Found ${queueLogicalId} in CloudFormation stack`
126
+ : `Will create ${queueLogicalId} in stack`
127
+ );
128
+ }
129
+
130
+ // EXTERNAL: User wants to use an existing queue
131
+ if (userIntent === ResourceOwnership.EXTERNAL) {
132
+ const queueUrl = integration?.queue?.url;
133
+ if (!queueUrl) {
134
+ throw new Error(
135
+ `Integration '${integrationName}' configured with ownership=external but queue.url not provided`
136
+ );
137
+ }
138
+ return this.createExternalDecision(
139
+ queueUrl,
140
+ `Using external queue URL: ${queueUrl}`
141
+ );
142
+ }
143
+
144
+ // AUTO: Intelligent decision based on discovery
145
+ if (inStack) {
146
+ const stackResource = this.findInStack(queueLogicalId, discovery);
147
+ return this.createStackDecision(
148
+ stackResource.physicalId,
149
+ `Found ${queueLogicalId} in CloudFormation stack - will include definition (idempotent)`
150
+ );
151
+ }
152
+
153
+ // Default: Create in stack
154
+ return this.createStackDecision(
155
+ null,
156
+ `No existing ${queueLogicalId} found - will create in stack`
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Capitalize first letter of string (e.g., 'slack' -> 'Slack')
162
+ * @param {string} str - String to capitalize
163
+ * @returns {string} Capitalized string
164
+ */
165
+ capitalizeFirst(str) {
166
+ return str.charAt(0).toUpperCase() + str.slice(1);
167
+ }
168
+ }
169
+
170
+ module.exports = IntegrationResourceResolver;
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Tests for IntegrationResourceResolver
3
+ */
4
+
5
+ const IntegrationResourceResolver = require('./integration-resolver');
6
+ const { ResourceOwnership } = require('../shared/types/resource-ownership');
7
+
8
+ describe('IntegrationResourceResolver', () => {
9
+ let resolver;
10
+
11
+ beforeEach(() => {
12
+ resolver = new IntegrationResourceResolver();
13
+ });
14
+
15
+ describe('resolveAll', () => {
16
+ it('should resolve InternalErrorQueue and per-integration queues', () => {
17
+ const appDef = {
18
+ integrations: [
19
+ { Definition: { name: 'slack' } },
20
+ { Definition: { name: 'hubspot' } },
21
+ ],
22
+ };
23
+
24
+ const discovery = {
25
+ fromCloudFormationStack: false,
26
+ stackName: null,
27
+ existingLogicalIds: [],
28
+ stackManaged: [],
29
+ stackResources: {},
30
+ external: [],
31
+ };
32
+
33
+ const decisions = resolver.resolveAll(appDef, discovery);
34
+
35
+ expect(decisions.internalErrorQueue).toBeDefined();
36
+ expect(decisions.internalErrorQueue.ownership).toBe(ResourceOwnership.STACK);
37
+ expect(decisions.integrations.slack).toBeDefined();
38
+ expect(decisions.integrations.slack.queue.ownership).toBe(ResourceOwnership.STACK);
39
+ expect(decisions.integrations.hubspot).toBeDefined();
40
+ expect(decisions.integrations.hubspot.queue.ownership).toBe(ResourceOwnership.STACK);
41
+ });
42
+
43
+ it('should skip integrations without Definition.name', () => {
44
+ const appDef = {
45
+ integrations: [
46
+ { Definition: { name: 'slack' } },
47
+ { Definition: {} }, // Missing name
48
+ { something: 'else' }, // Missing Definition
49
+ ],
50
+ };
51
+
52
+ const discovery = {
53
+ fromCloudFormationStack: false,
54
+ stackName: null,
55
+ existingLogicalIds: [],
56
+ stackManaged: [],
57
+ stackResources: {},
58
+ external: [],
59
+ };
60
+
61
+ const decisions = resolver.resolveAll(appDef, discovery);
62
+
63
+ expect(decisions.integrations.slack).toBeDefined();
64
+ expect(Object.keys(decisions.integrations)).toHaveLength(1);
65
+ });
66
+ });
67
+
68
+ describe('resolveInternalErrorQueue', () => {
69
+ it('should create in stack when nothing exists (AUTO)', () => {
70
+ const appDef = { integrations: [] };
71
+ const discovery = {
72
+ fromCloudFormationStack: false,
73
+ stackName: null,
74
+ existingLogicalIds: [],
75
+ stackManaged: [],
76
+ stackResources: {},
77
+ external: [],
78
+ };
79
+
80
+ const decision = resolver.resolveInternalErrorQueue(appDef, discovery);
81
+
82
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
83
+ expect(decision.physicalId).toBeNull();
84
+ expect(decision.reason).toContain('will create in stack');
85
+ });
86
+
87
+ it('should use stack resource when found (AUTO)', () => {
88
+ const appDef = { integrations: [] };
89
+ const discovery = {
90
+ fromCloudFormationStack: true,
91
+ stackName: 'test-stack',
92
+ existingLogicalIds: ['InternalErrorQueue'],
93
+ stackManaged: [
94
+ {
95
+ logicalId: 'InternalErrorQueue',
96
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/internal-error-queue',
97
+ type: 'AWS::SQS::Queue',
98
+ },
99
+ ],
100
+ stackResources: {
101
+ InternalErrorQueue: {
102
+ logicalId: 'InternalErrorQueue',
103
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/internal-error-queue',
104
+ type: 'AWS::SQS::Queue',
105
+ },
106
+ },
107
+ external: [],
108
+ };
109
+
110
+ const decision = resolver.resolveInternalErrorQueue(appDef, discovery);
111
+
112
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
113
+ expect(decision.physicalId).toBe('https://sqs.us-east-1.amazonaws.com/123456789/internal-error-queue');
114
+ expect(decision.reason).toContain('Found InternalErrorQueue in CloudFormation stack');
115
+ });
116
+
117
+ it('should respect explicit STACK ownership', () => {
118
+ const appDef = {
119
+ integrations: {
120
+ ownership: {
121
+ internalErrorQueue: ResourceOwnership.STACK,
122
+ },
123
+ },
124
+ };
125
+ const discovery = {
126
+ fromCloudFormationStack: false,
127
+ stackName: null,
128
+ existingLogicalIds: [],
129
+ stackManaged: [],
130
+ stackResources: {},
131
+ external: [],
132
+ };
133
+
134
+ const decision = resolver.resolveInternalErrorQueue(appDef, discovery);
135
+
136
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
137
+ expect(decision.physicalId).toBeNull();
138
+ });
139
+
140
+ it('should use external ARN when ownership=EXTERNAL', () => {
141
+ const appDef = {
142
+ integrations: {
143
+ ownership: {
144
+ internalErrorQueue: ResourceOwnership.EXTERNAL,
145
+ },
146
+ internalErrorQueue: {
147
+ arn: 'arn:aws:sqs:us-east-1:123456789:my-error-queue',
148
+ },
149
+ },
150
+ };
151
+ const discovery = {
152
+ fromCloudFormationStack: false,
153
+ stackName: null,
154
+ existingLogicalIds: [],
155
+ stackManaged: [],
156
+ stackResources: {},
157
+ external: [],
158
+ };
159
+
160
+ const decision = resolver.resolveInternalErrorQueue(appDef, discovery);
161
+
162
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
163
+ expect(decision.physicalId).toBe('arn:aws:sqs:us-east-1:123456789:my-error-queue');
164
+ expect(decision.reason).toContain('Using external InternalErrorQueue ARN');
165
+ });
166
+
167
+ it('should throw when EXTERNAL ownership but no ARN provided', () => {
168
+ const appDef = {
169
+ integrations: {
170
+ ownership: {
171
+ internalErrorQueue: ResourceOwnership.EXTERNAL,
172
+ },
173
+ },
174
+ };
175
+ const discovery = {
176
+ fromCloudFormationStack: false,
177
+ stackName: null,
178
+ existingLogicalIds: [],
179
+ stackManaged: [],
180
+ stackResources: {},
181
+ external: [],
182
+ };
183
+
184
+ expect(() => {
185
+ resolver.resolveInternalErrorQueue(appDef, discovery);
186
+ }).toThrow('InternalErrorQueue configured with ownership=external');
187
+ });
188
+ });
189
+
190
+ describe('resolveQueue', () => {
191
+ it('should create in stack when nothing exists (AUTO)', () => {
192
+ const appDef = {
193
+ integrations: [{ Definition: { name: 'slack' } }],
194
+ };
195
+ const discovery = {
196
+ fromCloudFormationStack: false,
197
+ stackName: null,
198
+ existingLogicalIds: [],
199
+ stackManaged: [],
200
+ stackResources: {},
201
+ external: [],
202
+ };
203
+
204
+ const decision = resolver.resolveQueue('slack', appDef, discovery);
205
+
206
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
207
+ expect(decision.physicalId).toBeNull();
208
+ expect(decision.reason).toContain('will create in stack');
209
+ });
210
+
211
+ it('should use stack resource when found (AUTO)', () => {
212
+ const appDef = {
213
+ integrations: [{ Definition: { name: 'slack' } }],
214
+ };
215
+ const discovery = {
216
+ fromCloudFormationStack: true,
217
+ stackName: 'test-stack',
218
+ existingLogicalIds: ['SlackQueue'],
219
+ stackManaged: [
220
+ {
221
+ logicalId: 'SlackQueue',
222
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/slack-queue',
223
+ type: 'AWS::SQS::Queue',
224
+ },
225
+ ],
226
+ stackResources: {
227
+ SlackQueue: {
228
+ logicalId: 'SlackQueue',
229
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/slack-queue',
230
+ type: 'AWS::SQS::Queue',
231
+ },
232
+ },
233
+ external: [],
234
+ };
235
+
236
+ const decision = resolver.resolveQueue('slack', appDef, discovery);
237
+
238
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
239
+ expect(decision.physicalId).toBe('https://sqs.us-east-1.amazonaws.com/123456789/slack-queue');
240
+ expect(decision.reason).toContain('Found SlackQueue in CloudFormation stack');
241
+ });
242
+
243
+ it('should respect per-integration STACK ownership', () => {
244
+ const appDef = {
245
+ integrations: [
246
+ {
247
+ Definition: { name: 'slack' },
248
+ ownership: { queue: ResourceOwnership.STACK },
249
+ },
250
+ ],
251
+ };
252
+ const discovery = {
253
+ fromCloudFormationStack: false,
254
+ stackName: null,
255
+ existingLogicalIds: [],
256
+ stackManaged: [],
257
+ stackResources: {},
258
+ external: [],
259
+ };
260
+
261
+ const decision = resolver.resolveQueue('slack', appDef, discovery);
262
+
263
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
264
+ expect(decision.physicalId).toBeNull();
265
+ });
266
+
267
+ it('should use external URL when ownership=EXTERNAL', () => {
268
+ const appDef = {
269
+ integrations: [
270
+ {
271
+ Definition: { name: 'slack' },
272
+ ownership: { queue: ResourceOwnership.EXTERNAL },
273
+ queue: { url: 'https://sqs.us-east-1.amazonaws.com/123456789/my-slack-queue' },
274
+ },
275
+ ],
276
+ };
277
+ const discovery = {
278
+ fromCloudFormationStack: false,
279
+ stackName: null,
280
+ existingLogicalIds: [],
281
+ stackManaged: [],
282
+ stackResources: {},
283
+ external: [],
284
+ };
285
+
286
+ const decision = resolver.resolveQueue('slack', appDef, discovery);
287
+
288
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
289
+ expect(decision.physicalId).toBe('https://sqs.us-east-1.amazonaws.com/123456789/my-slack-queue');
290
+ expect(decision.reason).toContain('Using external queue URL');
291
+ });
292
+
293
+ it('should throw when EXTERNAL ownership but no URL provided', () => {
294
+ const appDef = {
295
+ integrations: [
296
+ {
297
+ Definition: { name: 'slack' },
298
+ ownership: { queue: ResourceOwnership.EXTERNAL },
299
+ },
300
+ ],
301
+ };
302
+ const discovery = {
303
+ fromCloudFormationStack: false,
304
+ stackName: null,
305
+ existingLogicalIds: [],
306
+ stackManaged: [],
307
+ stackResources: {},
308
+ external: [],
309
+ };
310
+
311
+ expect(() => {
312
+ resolver.resolveQueue('slack', appDef, discovery);
313
+ }).toThrow("Integration 'slack' configured with ownership=external but queue.url not provided");
314
+ });
315
+
316
+ it('should handle multiple integrations with different queues', () => {
317
+ const appDef = {
318
+ integrations: [
319
+ { Definition: { name: 'slack' } },
320
+ { Definition: { name: 'hubspot' } },
321
+ ],
322
+ };
323
+ const discovery = {
324
+ fromCloudFormationStack: true,
325
+ stackName: 'test-stack',
326
+ existingLogicalIds: ['SlackQueue'],
327
+ stackManaged: [
328
+ {
329
+ logicalId: 'SlackQueue',
330
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/slack-queue',
331
+ type: 'AWS::SQS::Queue',
332
+ },
333
+ ],
334
+ stackResources: {
335
+ SlackQueue: {
336
+ logicalId: 'SlackQueue',
337
+ physicalId: 'https://sqs.us-east-1.amazonaws.com/123456789/slack-queue',
338
+ type: 'AWS::SQS::Queue',
339
+ },
340
+ },
341
+ external: [],
342
+ };
343
+
344
+ const slackDecision = resolver.resolveQueue('slack', appDef, discovery);
345
+ const hubspotDecision = resolver.resolveQueue('hubspot', appDef, discovery);
346
+
347
+ expect(slackDecision.ownership).toBe(ResourceOwnership.STACK);
348
+ expect(slackDecision.physicalId).toBe('https://sqs.us-east-1.amazonaws.com/123456789/slack-queue');
349
+ expect(hubspotDecision.ownership).toBe(ResourceOwnership.STACK);
350
+ expect(hubspotDecision.physicalId).toBeNull(); // HubspotQueue not in stack
351
+ });
352
+ });
353
+
354
+ describe('capitalizeFirst', () => {
355
+ it('should capitalize first letter', () => {
356
+ expect(resolver.capitalizeFirst('slack')).toBe('Slack');
357
+ expect(resolver.capitalizeFirst('hubspot')).toBe('Hubspot');
358
+ expect(resolver.capitalizeFirst('s')).toBe('S');
359
+ });
360
+
361
+ it('should handle already capitalized strings', () => {
362
+ expect(resolver.capitalizeFirst('Slack')).toBe('Slack');
363
+ });
364
+
365
+ it('should handle empty string', () => {
366
+ expect(resolver.capitalizeFirst('')).toBe('');
367
+ });
368
+ });
369
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * WebSocket Builder
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for:
7
+ * - WebSocket API Gateway configuration
8
+ * - WebSocket route handlers ($connect, $default, $disconnect)
9
+ * - WebSocket function definitions
10
+ */
11
+
12
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
13
+
14
+ class WebsocketBuilder extends InfrastructureBuilder {
15
+ constructor() {
16
+ super();
17
+ this.name = 'WebsocketBuilder';
18
+ }
19
+
20
+ shouldExecute(appDefinition) {
21
+ // Skip WebSocket in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
22
+ // API Gateway WebSocket is an AWS-specific service that should only be created in production
23
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
24
+ return false;
25
+ }
26
+
27
+ return appDefinition.websockets?.enable === true;
28
+ }
29
+
30
+ validate(appDefinition) {
31
+ const result = new ValidationResult();
32
+
33
+ if (!appDefinition.websockets) {
34
+ result.addError('WebSocket configuration is missing');
35
+ return result;
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ /**
42
+ * Build WebSocket infrastructure
43
+ */
44
+ async build(appDefinition, discoveredResources) {
45
+ console.log(`\n[${this.name}] Configuring WebSocket API Gateway...`);
46
+
47
+ const result = {
48
+ functions: {},
49
+ };
50
+
51
+ // Create default WebSocket handler
52
+ result.functions.defaultWebsocket = {
53
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
54
+ events: [
55
+ { websocket: { route: '$connect' } },
56
+ { websocket: { route: '$default' } },
57
+ { websocket: { route: '$disconnect' } },
58
+ ],
59
+ };
60
+
61
+ console.log(' ✅ WebSocket functions configured ($connect, $default, $disconnect)');
62
+ console.log(`[${this.name}] ✅ WebSocket configuration completed`);
63
+
64
+ return result;
65
+ }
66
+ }
67
+
68
+ module.exports = { WebsocketBuilder };
69
+