@friggframework/devtools 2.0.0--canary.461.849e166.0 → 2.0.0--canary.474.aa465e4.0

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 (32) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/domains/database/aurora-builder.js +234 -57
  3. package/infrastructure/domains/database/aurora-builder.test.js +7 -2
  4. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  5. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  6. package/infrastructure/domains/database/migration-builder.js +256 -215
  7. package/infrastructure/domains/database/migration-builder.test.js +5 -111
  8. package/infrastructure/domains/database/migration-resolver.js +163 -0
  9. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  10. package/infrastructure/domains/integration/integration-builder.js +258 -84
  11. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  12. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  13. package/infrastructure/domains/networking/vpc-builder.js +856 -135
  14. package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
  15. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  16. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  17. package/infrastructure/domains/security/kms-builder.js +179 -22
  18. package/infrastructure/domains/security/kms-resolver.js +96 -0
  19. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  20. package/infrastructure/domains/shared/base-resolver.js +186 -0
  21. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  22. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  23. package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
  24. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  25. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  26. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  27. package/infrastructure/domains/shared/types/index.js +46 -0
  28. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  29. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  30. package/package.json +6 -6
  31. package/infrastructure/REFACTOR.md +0 -532
  32. package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Base Resource Resolver
3
+ *
4
+ * Abstract base class for resource ownership resolution.
5
+ * Each builder has its own resolver (VpcResolver, AuroraResolver, etc.)
6
+ * that extends this base class.
7
+ *
8
+ * Resolver Layer - Hexagonal Architecture
9
+ */
10
+
11
+ const {
12
+ ResourceOwnership,
13
+ resolveOwnership,
14
+ findStackResource,
15
+ findExternalResource,
16
+ findAllExternalResources,
17
+ isResourceInStack
18
+ } = require('./types');
19
+
20
+ class BaseResourceResolver {
21
+ /**
22
+ * Find resource in CloudFormation stack
23
+ * @protected
24
+ * @param {string} logicalId - Logical resource ID
25
+ * @param {Object} discovery - Discovery result
26
+ * @returns {Object|null} Stack resource or null
27
+ */
28
+ findInStack(logicalId, discovery) {
29
+ return findStackResource(discovery, logicalId);
30
+ }
31
+
32
+ /**
33
+ * Find external resource by type
34
+ * @protected
35
+ * @param {string} resourceType - CloudFormation resource type
36
+ * @param {Object} discovery - Discovery result
37
+ * @returns {Object|null} External resource or null
38
+ */
39
+ findExternal(resourceType, discovery) {
40
+ return findExternalResource(discovery, resourceType);
41
+ }
42
+
43
+ /**
44
+ * Find all external resources by type
45
+ * @protected
46
+ * @param {Object} discovery - Discovery result
47
+ * @param {string} resourceType - CloudFormation resource type
48
+ * @returns {Object[]} Array of external resources
49
+ */
50
+ findAllExternalResources(discovery, resourceType) {
51
+ return findAllExternalResources(discovery, resourceType);
52
+ }
53
+
54
+ /**
55
+ * Check if resource is in stack
56
+ * @protected
57
+ * @param {string} logicalId - Logical resource ID
58
+ * @param {Object} discovery - Discovery result
59
+ * @returns {boolean}
60
+ */
61
+ isInStack(logicalId, discovery) {
62
+ return isResourceInStack(discovery, logicalId);
63
+ }
64
+
65
+ /**
66
+ * Validate that external resource IDs are provided when required
67
+ * @protected
68
+ * @param {*} resourceIds - Resource IDs to validate
69
+ * @param {string} resourceName - Name for error message
70
+ * @throws {Error} If resourceIds is not provided
71
+ */
72
+ requireExternalIds(resourceIds, resourceName) {
73
+ if (!resourceIds || (Array.isArray(resourceIds) && resourceIds.length === 0)) {
74
+ throw new Error(
75
+ `ownership='external' for ${resourceName} requires external.${resourceName} to be provided in app definition`
76
+ );
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Resolve ownership for a resource
82
+ * @protected
83
+ * @param {string} userIntent - User's ownership intent ('stack' | 'external' | 'auto')
84
+ * @param {string} logicalId - CloudFormation logical ID
85
+ * @param {string} resourceType - CloudFormation resource type
86
+ * @param {Object} discovery - Discovery result
87
+ * @returns {Object} Resource decision
88
+ */
89
+ resolveResourceOwnership(userIntent, logicalId, resourceType, discovery) {
90
+ // Use helper to work with both old flat structure and new structured
91
+ const structured = discovery._structured || discovery;
92
+
93
+ const inStack = this.isInStack(logicalId, structured);
94
+ const externalResource = this.findExternal(resourceType, structured);
95
+
96
+ const ownership = resolveOwnership(
97
+ userIntent || ResourceOwnership.AUTO,
98
+ inStack,
99
+ externalResource !== null
100
+ );
101
+
102
+ const stackResource = inStack ? this.findInStack(logicalId, structured) : null;
103
+
104
+ return {
105
+ ownership,
106
+ physicalId: stackResource?.physicalId || externalResource?.physicalId,
107
+ reason: this._buildReasonString(ownership, inStack, externalResource, userIntent),
108
+ metadata: {
109
+ logicalId,
110
+ resourceType,
111
+ userIntent: userIntent || 'auto',
112
+ inStack,
113
+ foundExternal: externalResource !== null
114
+ }
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Build human-readable reason string
120
+ * @private
121
+ */
122
+ _buildReasonString(ownership, inStack, externalResource, userIntent) {
123
+ if (userIntent === 'stack') {
124
+ return 'User explicitly specified ownership=stack';
125
+ }
126
+
127
+ if (userIntent === 'external') {
128
+ return 'User explicitly specified ownership=external';
129
+ }
130
+
131
+ // Auto-decided
132
+ if (ownership === ResourceOwnership.STACK) {
133
+ if (inStack) {
134
+ return 'Found in CloudFormation stack (must keep in template to avoid deletion)';
135
+ }
136
+ return 'No existing resource found - will create in stack';
137
+ }
138
+
139
+ if (ownership === ResourceOwnership.EXTERNAL) {
140
+ return 'Found external resource via discovery';
141
+ }
142
+
143
+ return 'Ownership resolved via auto-detection';
144
+ }
145
+
146
+ /**
147
+ * Create a resource decision for explicit external reference
148
+ * @protected
149
+ * @param {string|string[]} physicalIds - Physical resource ID(s)
150
+ * @param {string} reason - Reason string
151
+ * @returns {Object} Resource decision
152
+ */
153
+ createExternalDecision(physicalIds, reason = 'Using external resource reference') {
154
+ const ids = Array.isArray(physicalIds) ? physicalIds : [physicalIds];
155
+
156
+ return {
157
+ ownership: ResourceOwnership.EXTERNAL,
158
+ physicalId: ids[0],
159
+ physicalIds: ids,
160
+ reason,
161
+ metadata: {
162
+ source: 'user-provided'
163
+ }
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Create a resource decision for stack-managed resource
169
+ * @protected
170
+ * @param {string} [physicalId] - Physical ID if resource already exists
171
+ * @param {string} reason - Reason string
172
+ * @returns {Object} Resource decision
173
+ */
174
+ createStackDecision(physicalId = null, reason = 'Managed by CloudFormation stack') {
175
+ return {
176
+ ownership: ResourceOwnership.STACK,
177
+ physicalId,
178
+ reason,
179
+ metadata: {
180
+ source: physicalId ? 'discovered' : 'new'
181
+ }
182
+ };
183
+ }
184
+ }
185
+
186
+ module.exports = BaseResourceResolver;
@@ -0,0 +1,305 @@
1
+ const BaseResourceResolver = require('./base-resolver');
2
+ const { ResourceOwnership } = require('./types');
3
+
4
+ describe('BaseResourceResolver', () => {
5
+ let resolver;
6
+
7
+ beforeEach(() => {
8
+ resolver = new BaseResourceResolver();
9
+ });
10
+
11
+ describe('helper methods', () => {
12
+ const mockDiscovery = {
13
+ stackManaged: [
14
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
15
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' }
16
+ ],
17
+ external: [
18
+ { physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }
19
+ ],
20
+ fromCloudFormation: true
21
+ };
22
+
23
+ describe('findInStack', () => {
24
+ it('should find resource in stack', () => {
25
+ const resource = resolver.findInStack('FriggVPC', mockDiscovery);
26
+
27
+ expect(resource).toEqual({
28
+ logicalId: 'FriggVPC',
29
+ physicalId: 'vpc-123',
30
+ resourceType: 'AWS::EC2::VPC'
31
+ });
32
+ });
33
+
34
+ it('should return null if not found', () => {
35
+ const resource = resolver.findInStack('NonExistent', mockDiscovery);
36
+ expect(resource).toBeNull();
37
+ });
38
+ });
39
+
40
+ describe('findExternal', () => {
41
+ it('should find external resource by type', () => {
42
+ const resource = resolver.findExternal('AWS::EC2::VPC', mockDiscovery);
43
+
44
+ expect(resource).toEqual({
45
+ physicalId: 'vpc-external',
46
+ resourceType: 'AWS::EC2::VPC',
47
+ source: 'tag-search'
48
+ });
49
+ });
50
+
51
+ it('should return null if not found', () => {
52
+ const resource = resolver.findExternal('AWS::RDS::DBCluster', mockDiscovery);
53
+ expect(resource).toBeNull();
54
+ });
55
+ });
56
+
57
+ describe('isInStack', () => {
58
+ it('should return true if resource is in stack', () => {
59
+ expect(resolver.isInStack('FriggVPC', mockDiscovery)).toBe(true);
60
+ expect(resolver.isInStack('FriggLambdaSecurityGroup', mockDiscovery)).toBe(true);
61
+ });
62
+
63
+ it('should return false if resource is not in stack', () => {
64
+ expect(resolver.isInStack('NonExistent', mockDiscovery)).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('requireExternalIds', () => {
69
+ it('should not throw if IDs are provided', () => {
70
+ expect(() => resolver.requireExternalIds('vpc-123', 'vpcId')).not.toThrow();
71
+ expect(() => resolver.requireExternalIds(['sg-1', 'sg-2'], 'securityGroupIds')).not.toThrow();
72
+ });
73
+
74
+ it('should throw if IDs are missing', () => {
75
+ expect(() => resolver.requireExternalIds(undefined, 'vpcId')).toThrow(
76
+ "ownership='external' for vpcId requires external.vpcId"
77
+ );
78
+ expect(() => resolver.requireExternalIds(null, 'vpcId')).toThrow();
79
+ expect(() => resolver.requireExternalIds([], 'securityGroupIds')).toThrow();
80
+ });
81
+ });
82
+ });
83
+
84
+ describe('resolveResourceOwnership', () => {
85
+ describe('with explicit stack intent', () => {
86
+ it('should return STACK ownership', () => {
87
+ const discovery = {
88
+ stackManaged: [],
89
+ external: [{ physicalId: 'vpc-ext', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }],
90
+ fromCloudFormation: false
91
+ };
92
+
93
+ const decision = resolver.resolveResourceOwnership(
94
+ 'stack',
95
+ 'FriggVPC',
96
+ 'AWS::EC2::VPC',
97
+ discovery
98
+ );
99
+
100
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
101
+ expect(decision.reason).toContain('User explicitly specified ownership=stack');
102
+ });
103
+ });
104
+
105
+ describe('with explicit external intent', () => {
106
+ it('should return EXTERNAL ownership', () => {
107
+ const discovery = {
108
+ stackManaged: [
109
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' }
110
+ ],
111
+ external: [],
112
+ fromCloudFormation: true
113
+ };
114
+
115
+ const decision = resolver.resolveResourceOwnership(
116
+ 'external',
117
+ 'FriggVPC',
118
+ 'AWS::EC2::VPC',
119
+ discovery
120
+ );
121
+
122
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
123
+ expect(decision.reason).toContain('User explicitly specified ownership=external');
124
+ });
125
+ });
126
+
127
+ describe('with auto intent', () => {
128
+ it('should return STACK if resource is in stack (CRITICAL)', () => {
129
+ const discovery = {
130
+ stackManaged: [
131
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' }
132
+ ],
133
+ external: [],
134
+ fromCloudFormation: true
135
+ };
136
+
137
+ const decision = resolver.resolveResourceOwnership(
138
+ 'auto',
139
+ 'FriggLambdaSecurityGroup',
140
+ 'AWS::EC2::SecurityGroup',
141
+ discovery
142
+ );
143
+
144
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
145
+ expect(decision.physicalId).toBe('sg-069629001ade41c9a');
146
+ expect(decision.reason).toContain('Found in CloudFormation stack');
147
+ expect(decision.reason).toContain('must keep in template to avoid deletion');
148
+ });
149
+
150
+ it('should return EXTERNAL if found externally but not in stack', () => {
151
+ const discovery = {
152
+ stackManaged: [],
153
+ external: [
154
+ { physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }
155
+ ],
156
+ fromCloudFormation: false
157
+ };
158
+
159
+ const decision = resolver.resolveResourceOwnership(
160
+ 'auto',
161
+ 'FriggVPC',
162
+ 'AWS::EC2::VPC',
163
+ discovery
164
+ );
165
+
166
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
167
+ expect(decision.physicalId).toBe('vpc-external');
168
+ expect(decision.reason).toContain('Found external resource via discovery');
169
+ });
170
+
171
+ it('should return STACK if not found anywhere (create new)', () => {
172
+ const discovery = {
173
+ stackManaged: [],
174
+ external: [],
175
+ fromCloudFormation: false
176
+ };
177
+
178
+ const decision = resolver.resolveResourceOwnership(
179
+ 'auto',
180
+ 'FriggVPC',
181
+ 'AWS::EC2::VPC',
182
+ discovery
183
+ );
184
+
185
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
186
+ expect(decision.physicalId).toBeUndefined();
187
+ expect(decision.reason).toContain('No existing resource found - will create in stack');
188
+ });
189
+ });
190
+
191
+ describe('metadata', () => {
192
+ it('should include complete metadata', () => {
193
+ const discovery = {
194
+ stackManaged: [
195
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' }
196
+ ],
197
+ external: [],
198
+ fromCloudFormation: true
199
+ };
200
+
201
+ const decision = resolver.resolveResourceOwnership(
202
+ 'auto',
203
+ 'FriggVPC',
204
+ 'AWS::EC2::VPC',
205
+ discovery
206
+ );
207
+
208
+ expect(decision.metadata).toEqual({
209
+ logicalId: 'FriggVPC',
210
+ resourceType: 'AWS::EC2::VPC',
211
+ userIntent: 'auto',
212
+ inStack: true,
213
+ foundExternal: false
214
+ });
215
+ });
216
+ });
217
+ });
218
+
219
+ describe('createExternalDecision', () => {
220
+ it('should create external decision with single ID', () => {
221
+ const decision = resolver.createExternalDecision('vpc-external');
222
+
223
+ expect(decision).toEqual({
224
+ ownership: ResourceOwnership.EXTERNAL,
225
+ physicalId: 'vpc-external',
226
+ physicalIds: ['vpc-external'],
227
+ reason: 'Using external resource reference',
228
+ metadata: {
229
+ source: 'user-provided'
230
+ }
231
+ });
232
+ });
233
+
234
+ it('should create external decision with multiple IDs', () => {
235
+ const decision = resolver.createExternalDecision(['sg-1', 'sg-2'], 'Custom reason');
236
+
237
+ expect(decision).toEqual({
238
+ ownership: ResourceOwnership.EXTERNAL,
239
+ physicalId: 'sg-1',
240
+ physicalIds: ['sg-1', 'sg-2'],
241
+ reason: 'Custom reason',
242
+ metadata: {
243
+ source: 'user-provided'
244
+ }
245
+ });
246
+ });
247
+ });
248
+
249
+ describe('createStackDecision', () => {
250
+ it('should create stack decision for new resource', () => {
251
+ const decision = resolver.createStackDecision();
252
+
253
+ expect(decision).toEqual({
254
+ ownership: ResourceOwnership.STACK,
255
+ physicalId: null,
256
+ reason: 'Managed by CloudFormation stack',
257
+ metadata: {
258
+ source: 'new'
259
+ }
260
+ });
261
+ });
262
+
263
+ it('should create stack decision for existing resource', () => {
264
+ const decision = resolver.createStackDecision('vpc-123', 'Found in stack');
265
+
266
+ expect(decision).toEqual({
267
+ ownership: ResourceOwnership.STACK,
268
+ physicalId: 'vpc-123',
269
+ reason: 'Found in stack',
270
+ metadata: {
271
+ source: 'discovered'
272
+ }
273
+ });
274
+ });
275
+ });
276
+
277
+ describe('backwards compatibility with flat discovery', () => {
278
+ it('should work with old flat discovery structure', () => {
279
+ const flatDiscovery = {
280
+ fromCloudFormationStack: true,
281
+ existingLogicalIds: ['FriggVPC', 'FriggLambdaSecurityGroup'],
282
+ defaultVpcId: 'vpc-123',
283
+ securityGroupId: 'sg-456',
284
+ _structured: {
285
+ stackManaged: [
286
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
287
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' }
288
+ ],
289
+ external: [],
290
+ fromCloudFormation: true
291
+ }
292
+ };
293
+
294
+ const decision = resolver.resolveResourceOwnership(
295
+ 'auto',
296
+ 'FriggLambdaSecurityGroup',
297
+ 'AWS::EC2::SecurityGroup',
298
+ flatDiscovery
299
+ );
300
+
301
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
302
+ expect(decision.physicalId).toBe('sg-456');
303
+ });
304
+ });
305
+ });