@friggframework/devtools 2.0.0--canary.474.27d9425.0 → 2.0.0--canary.474.da7b114.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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * ResourceState Value Object
3
+ *
4
+ * Enum-like immutable state for CloudFormation resources
5
+ *
6
+ * States:
7
+ * - IN_STACK: Resource exists in CloudFormation stack and matches expected definition
8
+ * - ORPHANED: Resource exists in AWS but not in CloudFormation stack
9
+ * - MISSING: Resource is in CloudFormation stack but not in AWS
10
+ * - DRIFTED: Resource exists in both but properties differ
11
+ * - EXTERNAL: Resource is intentionally external to stack
12
+ */
13
+
14
+ class ResourceState {
15
+ /**
16
+ * Valid resource states
17
+ * @type {string[]}
18
+ */
19
+ static VALID_STATES = [
20
+ 'IN_STACK',
21
+ 'ORPHANED',
22
+ 'MISSING',
23
+ 'DRIFTED',
24
+ 'EXTERNAL',
25
+ ];
26
+
27
+ /**
28
+ * Create a new ResourceState
29
+ *
30
+ * @param {string} value - State value
31
+ */
32
+ constructor(value) {
33
+ if (value === undefined || value === null) {
34
+ throw new Error('Resource state is required');
35
+ }
36
+
37
+ if (!ResourceState.VALID_STATES.includes(value)) {
38
+ throw new Error(`Invalid resource state: ${value}`);
39
+ }
40
+
41
+ this._value = value;
42
+
43
+ // Make immutable
44
+ Object.freeze(this);
45
+ }
46
+
47
+ /**
48
+ * Get state value
49
+ * @returns {string}
50
+ */
51
+ get value() {
52
+ return this._value;
53
+ }
54
+
55
+ /**
56
+ * Prevent modification of value
57
+ * @throws {TypeError}
58
+ */
59
+ set value(newValue) {
60
+ throw new TypeError('Cannot modify immutable property value');
61
+ }
62
+
63
+ /**
64
+ * Check if resource is in stack
65
+ * @returns {boolean}
66
+ */
67
+ isInStack() {
68
+ return this._value === 'IN_STACK';
69
+ }
70
+
71
+ /**
72
+ * Check if resource is orphaned
73
+ * @returns {boolean}
74
+ */
75
+ isOrphaned() {
76
+ return this._value === 'ORPHANED';
77
+ }
78
+
79
+ /**
80
+ * Check if resource is missing
81
+ * @returns {boolean}
82
+ */
83
+ isMissing() {
84
+ return this._value === 'MISSING';
85
+ }
86
+
87
+ /**
88
+ * Check if resource has drifted
89
+ * @returns {boolean}
90
+ */
91
+ isDrifted() {
92
+ return this._value === 'DRIFTED';
93
+ }
94
+
95
+ /**
96
+ * Check if resource is external
97
+ * @returns {boolean}
98
+ */
99
+ isExternal() {
100
+ return this._value === 'EXTERNAL';
101
+ }
102
+
103
+ /**
104
+ * Check equality with another ResourceState
105
+ *
106
+ * @param {ResourceState} other
107
+ * @returns {boolean}
108
+ */
109
+ equals(other) {
110
+ if (!(other instanceof ResourceState)) {
111
+ return false;
112
+ }
113
+
114
+ return this._value === other._value;
115
+ }
116
+
117
+ /**
118
+ * Get string representation
119
+ *
120
+ * @returns {string}
121
+ */
122
+ toString() {
123
+ return this._value;
124
+ }
125
+
126
+ /**
127
+ * Predefined state: IN_STACK
128
+ * @type {ResourceState}
129
+ */
130
+ static get IN_STACK() {
131
+ return new ResourceState('IN_STACK');
132
+ }
133
+
134
+ /**
135
+ * Predefined state: ORPHANED
136
+ * @type {ResourceState}
137
+ */
138
+ static get ORPHANED() {
139
+ return new ResourceState('ORPHANED');
140
+ }
141
+
142
+ /**
143
+ * Predefined state: MISSING
144
+ * @type {ResourceState}
145
+ */
146
+ static get MISSING() {
147
+ return new ResourceState('MISSING');
148
+ }
149
+
150
+ /**
151
+ * Predefined state: DRIFTED
152
+ * @type {ResourceState}
153
+ */
154
+ static get DRIFTED() {
155
+ return new ResourceState('DRIFTED');
156
+ }
157
+
158
+ /**
159
+ * Predefined state: EXTERNAL
160
+ * @type {ResourceState}
161
+ */
162
+ static get EXTERNAL() {
163
+ return new ResourceState('EXTERNAL');
164
+ }
165
+ }
166
+
167
+ module.exports = ResourceState;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for ResourceState Value Object
3
+ */
4
+
5
+ const ResourceState = require('./resource-state');
6
+
7
+ describe('ResourceState', () => {
8
+ describe('valid states', () => {
9
+ it('should accept IN_STACK state', () => {
10
+ const state = new ResourceState('IN_STACK');
11
+
12
+ expect(state.value).toBe('IN_STACK');
13
+ });
14
+
15
+ it('should accept ORPHANED state', () => {
16
+ const state = new ResourceState('ORPHANED');
17
+
18
+ expect(state.value).toBe('ORPHANED');
19
+ });
20
+
21
+ it('should accept MISSING state', () => {
22
+ const state = new ResourceState('MISSING');
23
+
24
+ expect(state.value).toBe('MISSING');
25
+ });
26
+
27
+ it('should accept DRIFTED state', () => {
28
+ const state = new ResourceState('DRIFTED');
29
+
30
+ expect(state.value).toBe('DRIFTED');
31
+ });
32
+
33
+ it('should accept EXTERNAL state', () => {
34
+ const state = new ResourceState('EXTERNAL');
35
+
36
+ expect(state.value).toBe('EXTERNAL');
37
+ });
38
+ });
39
+
40
+ describe('invalid states', () => {
41
+ it('should reject invalid state', () => {
42
+ expect(() => {
43
+ new ResourceState('INVALID');
44
+ }).toThrow('Invalid resource state: INVALID');
45
+ });
46
+
47
+ it('should reject lowercase state', () => {
48
+ expect(() => {
49
+ new ResourceState('in_stack');
50
+ }).toThrow('Invalid resource state: in_stack');
51
+ });
52
+
53
+ it('should reject null', () => {
54
+ expect(() => {
55
+ new ResourceState(null);
56
+ }).toThrow('Resource state is required');
57
+ });
58
+
59
+ it('should reject undefined', () => {
60
+ expect(() => {
61
+ new ResourceState(undefined);
62
+ }).toThrow('Resource state is required');
63
+ });
64
+ });
65
+
66
+ describe('state checks', () => {
67
+ it('should check if resource is in stack', () => {
68
+ const state = new ResourceState('IN_STACK');
69
+
70
+ expect(state.isInStack()).toBe(true);
71
+ expect(state.isOrphaned()).toBe(false);
72
+ expect(state.isMissing()).toBe(false);
73
+ expect(state.isDrifted()).toBe(false);
74
+ expect(state.isExternal()).toBe(false);
75
+ });
76
+
77
+ it('should check if resource is orphaned', () => {
78
+ const state = new ResourceState('ORPHANED');
79
+
80
+ expect(state.isInStack()).toBe(false);
81
+ expect(state.isOrphaned()).toBe(true);
82
+ expect(state.isMissing()).toBe(false);
83
+ expect(state.isDrifted()).toBe(false);
84
+ expect(state.isExternal()).toBe(false);
85
+ });
86
+
87
+ it('should check if resource is missing', () => {
88
+ const state = new ResourceState('MISSING');
89
+
90
+ expect(state.isInStack()).toBe(false);
91
+ expect(state.isOrphaned()).toBe(false);
92
+ expect(state.isMissing()).toBe(true);
93
+ expect(state.isDrifted()).toBe(false);
94
+ expect(state.isExternal()).toBe(false);
95
+ });
96
+
97
+ it('should check if resource is drifted', () => {
98
+ const state = new ResourceState('DRIFTED');
99
+
100
+ expect(state.isInStack()).toBe(false);
101
+ expect(state.isOrphaned()).toBe(false);
102
+ expect(state.isMissing()).toBe(false);
103
+ expect(state.isDrifted()).toBe(true);
104
+ expect(state.isExternal()).toBe(false);
105
+ });
106
+
107
+ it('should check if resource is external', () => {
108
+ const state = new ResourceState('EXTERNAL');
109
+
110
+ expect(state.isInStack()).toBe(false);
111
+ expect(state.isOrphaned()).toBe(false);
112
+ expect(state.isMissing()).toBe(false);
113
+ expect(state.isDrifted()).toBe(false);
114
+ expect(state.isExternal()).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('equality', () => {
119
+ it('should be equal to same state', () => {
120
+ const state1 = new ResourceState('IN_STACK');
121
+ const state2 = new ResourceState('IN_STACK');
122
+
123
+ expect(state1.equals(state2)).toBe(true);
124
+ });
125
+
126
+ it('should not be equal to different state', () => {
127
+ const state1 = new ResourceState('IN_STACK');
128
+ const state2 = new ResourceState('ORPHANED');
129
+
130
+ expect(state1.equals(state2)).toBe(false);
131
+ });
132
+
133
+ it('should not be equal to non-ResourceState', () => {
134
+ const state = new ResourceState('IN_STACK');
135
+
136
+ expect(state.equals('IN_STACK')).toBe(false);
137
+ expect(state.equals(null)).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('toString', () => {
142
+ it('should return string representation', () => {
143
+ const state = new ResourceState('IN_STACK');
144
+
145
+ expect(state.toString()).toBe('IN_STACK');
146
+ });
147
+ });
148
+
149
+ describe('static constants', () => {
150
+ it('should provide IN_STACK constant', () => {
151
+ expect(ResourceState.IN_STACK.value).toBe('IN_STACK');
152
+ });
153
+
154
+ it('should provide ORPHANED constant', () => {
155
+ expect(ResourceState.ORPHANED.value).toBe('ORPHANED');
156
+ });
157
+
158
+ it('should provide MISSING constant', () => {
159
+ expect(ResourceState.MISSING.value).toBe('MISSING');
160
+ });
161
+
162
+ it('should provide DRIFTED constant', () => {
163
+ expect(ResourceState.DRIFTED.value).toBe('DRIFTED');
164
+ });
165
+
166
+ it('should provide EXTERNAL constant', () => {
167
+ expect(ResourceState.EXTERNAL.value).toBe('EXTERNAL');
168
+ });
169
+
170
+ it('should provide VALID_STATES array', () => {
171
+ expect(ResourceState.VALID_STATES).toEqual([
172
+ 'IN_STACK',
173
+ 'ORPHANED',
174
+ 'MISSING',
175
+ 'DRIFTED',
176
+ 'EXTERNAL',
177
+ ]);
178
+ });
179
+ });
180
+
181
+ describe('immutability', () => {
182
+ it('should not allow modification of value', () => {
183
+ const state = new ResourceState('IN_STACK');
184
+
185
+ expect(() => {
186
+ state.value = 'ORPHANED';
187
+ }).toThrow();
188
+ });
189
+
190
+ it('should be frozen', () => {
191
+ const state = new ResourceState('IN_STACK');
192
+
193
+ expect(Object.isFrozen(state)).toBe(true);
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * StackIdentifier Value Object
3
+ *
4
+ * Immutable identifier for a CloudFormation stack
5
+ * Combines stack name, region, and optional account ID
6
+ */
7
+
8
+ class StackIdentifier {
9
+ /**
10
+ * Valid AWS regions
11
+ * @private
12
+ */
13
+ static VALID_REGIONS = [
14
+ 'us-east-1',
15
+ 'us-east-2',
16
+ 'us-west-1',
17
+ 'us-west-2',
18
+ 'af-south-1',
19
+ 'ap-east-1',
20
+ 'ap-south-1',
21
+ 'ap-northeast-1',
22
+ 'ap-northeast-2',
23
+ 'ap-northeast-3',
24
+ 'ap-southeast-1',
25
+ 'ap-southeast-2',
26
+ 'ca-central-1',
27
+ 'eu-central-1',
28
+ 'eu-west-1',
29
+ 'eu-west-2',
30
+ 'eu-west-3',
31
+ 'eu-north-1',
32
+ 'eu-south-1',
33
+ 'me-south-1',
34
+ 'sa-east-1',
35
+ ];
36
+
37
+ /**
38
+ * Create a new StackIdentifier
39
+ *
40
+ * @param {Object} params
41
+ * @param {string} params.stackName - CloudFormation stack name
42
+ * @param {string} params.region - AWS region
43
+ * @param {string} [params.accountId] - AWS account ID (12 digits)
44
+ */
45
+ constructor({ stackName, region, accountId = null }) {
46
+ // Validate required fields
47
+ if (stackName === undefined || stackName === null) {
48
+ throw new Error('stackName is required');
49
+ }
50
+
51
+ if (region === undefined || region === null) {
52
+ throw new Error('region is required');
53
+ }
54
+
55
+ // Validate formats
56
+ if (typeof stackName === 'string' && stackName.trim() === '') {
57
+ throw new Error('stackName cannot be empty');
58
+ }
59
+
60
+ if (!StackIdentifier.VALID_REGIONS.includes(region)) {
61
+ throw new Error('region must be a valid AWS region');
62
+ }
63
+
64
+ if (accountId !== null && !/^\d{12}$/.test(accountId)) {
65
+ throw new Error('accountId must be a 12-digit number');
66
+ }
67
+
68
+ // Assign properties
69
+ this._stackName = stackName;
70
+ this._region = region;
71
+ this._accountId = accountId;
72
+
73
+ // Make immutable
74
+ Object.freeze(this);
75
+ }
76
+
77
+ /**
78
+ * Get stack name
79
+ * @returns {string}
80
+ */
81
+ get stackName() {
82
+ return this._stackName;
83
+ }
84
+
85
+ /**
86
+ * Prevent modification of stackName
87
+ * @throws {TypeError}
88
+ */
89
+ set stackName(value) {
90
+ throw new TypeError('Cannot modify immutable property stackName');
91
+ }
92
+
93
+ /**
94
+ * Get region
95
+ * @returns {string}
96
+ */
97
+ get region() {
98
+ return this._region;
99
+ }
100
+
101
+ /**
102
+ * Prevent modification of region
103
+ * @throws {TypeError}
104
+ */
105
+ set region(value) {
106
+ throw new TypeError('Cannot modify immutable property region');
107
+ }
108
+
109
+ /**
110
+ * Get account ID
111
+ * @returns {string|null}
112
+ */
113
+ get accountId() {
114
+ return this._accountId;
115
+ }
116
+
117
+ /**
118
+ * Prevent modification of accountId
119
+ * @throws {TypeError}
120
+ */
121
+ set accountId(value) {
122
+ throw new TypeError('Cannot modify immutable property accountId');
123
+ }
124
+
125
+ /**
126
+ * Check equality with another StackIdentifier
127
+ *
128
+ * @param {StackIdentifier} other
129
+ * @returns {boolean}
130
+ */
131
+ equals(other) {
132
+ if (!(other instanceof StackIdentifier)) {
133
+ return false;
134
+ }
135
+
136
+ return (
137
+ this.stackName === other.stackName &&
138
+ this.region === other.region &&
139
+ this.accountId === other.accountId
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Get string representation
145
+ *
146
+ * @returns {string}
147
+ */
148
+ toString() {
149
+ if (this.accountId) {
150
+ return `${this.stackName} (${this.region}, ${this.accountId})`;
151
+ }
152
+ return `${this.stackName} (${this.region})`;
153
+ }
154
+
155
+ /**
156
+ * Create StackIdentifier from ARN
157
+ *
158
+ * @param {string} arn - CloudFormation stack ARN
159
+ * @returns {StackIdentifier}
160
+ */
161
+ static fromArn(arn) {
162
+ // arn:aws:cloudformation:region:account-id:stack/stack-name/guid
163
+ const match = arn.match(/^arn:aws:cloudformation:([^:]+):(\d{12}):stack\/([^\/]+)/);
164
+
165
+ if (!match) {
166
+ throw new Error('Invalid CloudFormation stack ARN');
167
+ }
168
+
169
+ const [, region, accountId, stackName] = match;
170
+
171
+ return new StackIdentifier({
172
+ stackName,
173
+ region,
174
+ accountId,
175
+ });
176
+ }
177
+ }
178
+
179
+ module.exports = StackIdentifier;