@friggframework/devtools 2.0.0--canary.474.28f4860.0 → 2.0.0--canary.474.97bfcf0.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.
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +105 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +251 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +179 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +233 -0
- package/infrastructure/domains/security/kms-builder.js +10 -1
- package/package.json +6 -6
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyMismatch Entity
|
|
3
|
+
*
|
|
4
|
+
* Represents a difference between expected and actual property values
|
|
5
|
+
* for a CloudFormation resource.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
9
|
+
|
|
10
|
+
class PropertyMismatch {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new PropertyMismatch
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} params
|
|
15
|
+
* @param {string} params.propertyPath - Path to the property (e.g., 'Properties.BucketName')
|
|
16
|
+
* @param {*} params.expectedValue - Expected property value
|
|
17
|
+
* @param {*} params.actualValue - Actual property value
|
|
18
|
+
* @param {PropertyMutability} params.mutability - Property mutability
|
|
19
|
+
*/
|
|
20
|
+
constructor({ propertyPath, expectedValue, actualValue, mutability }) {
|
|
21
|
+
// Validate required fields
|
|
22
|
+
if (!propertyPath) {
|
|
23
|
+
throw new Error('propertyPath is required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (expectedValue === undefined) {
|
|
27
|
+
throw new Error('expectedValue is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (actualValue === undefined) {
|
|
31
|
+
throw new Error('actualValue is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!mutability) {
|
|
35
|
+
throw new Error('mutability is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!(mutability instanceof PropertyMutability)) {
|
|
39
|
+
throw new Error('mutability must be a PropertyMutability instance');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.propertyPath = propertyPath;
|
|
43
|
+
this.expectedValue = expectedValue;
|
|
44
|
+
this.actualValue = actualValue;
|
|
45
|
+
this.mutability = mutability;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if fixing this mismatch requires resource replacement
|
|
50
|
+
*
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
requiresReplacement() {
|
|
54
|
+
return this.mutability.requiresReplacement();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if this mismatch can be automatically fixed
|
|
59
|
+
*
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
canAutoFix() {
|
|
63
|
+
return this.mutability.canChange();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get severity level of this mismatch
|
|
68
|
+
*
|
|
69
|
+
* @returns {'critical' | 'warning'}
|
|
70
|
+
*/
|
|
71
|
+
getSeverity() {
|
|
72
|
+
return this.mutability.isImmutable() ? 'critical' : 'warning';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get string representation
|
|
77
|
+
*
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
toString() {
|
|
81
|
+
const expectedStr = this.expectedValue === null ? 'null' : this.expectedValue;
|
|
82
|
+
const actualStr = this.actualValue === null ? 'null' : this.actualValue;
|
|
83
|
+
|
|
84
|
+
return `PropertyMismatch: ${this.propertyPath} (expected: ${expectedStr}, actual: ${actualStr}, mutability: ${this.mutability.toString()})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serialize to JSON
|
|
89
|
+
*
|
|
90
|
+
* @returns {Object}
|
|
91
|
+
*/
|
|
92
|
+
toJSON() {
|
|
93
|
+
return {
|
|
94
|
+
propertyPath: this.propertyPath,
|
|
95
|
+
expectedValue: this.expectedValue,
|
|
96
|
+
actualValue: this.actualValue,
|
|
97
|
+
mutability: this.mutability.toString(),
|
|
98
|
+
severity: this.getSeverity(),
|
|
99
|
+
canAutoFix: this.canAutoFix(),
|
|
100
|
+
requiresReplacement: this.requiresReplacement(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = PropertyMismatch;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for PropertyMismatch Entity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PropertyMismatch = require('./property-mismatch');
|
|
6
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
7
|
+
|
|
8
|
+
describe('PropertyMismatch', () => {
|
|
9
|
+
describe('constructor', () => {
|
|
10
|
+
it('should create property mismatch with all fields', () => {
|
|
11
|
+
const mismatch = new PropertyMismatch({
|
|
12
|
+
propertyPath: 'Properties.BucketName',
|
|
13
|
+
expectedValue: 'my-app-prod-bucket',
|
|
14
|
+
actualValue: 'my-app-dev-bucket',
|
|
15
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(mismatch.propertyPath).toBe('Properties.BucketName');
|
|
19
|
+
expect(mismatch.expectedValue).toBe('my-app-prod-bucket');
|
|
20
|
+
expect(mismatch.actualValue).toBe('my-app-dev-bucket');
|
|
21
|
+
expect(mismatch.mutability.value).toBe('IMMUTABLE');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should require propertyPath', () => {
|
|
25
|
+
expect(() => {
|
|
26
|
+
new PropertyMismatch({
|
|
27
|
+
expectedValue: 'value1',
|
|
28
|
+
actualValue: 'value2',
|
|
29
|
+
mutability: PropertyMutability.MUTABLE,
|
|
30
|
+
});
|
|
31
|
+
}).toThrow('propertyPath is required');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should require expectedValue', () => {
|
|
35
|
+
expect(() => {
|
|
36
|
+
new PropertyMismatch({
|
|
37
|
+
propertyPath: 'Properties.Name',
|
|
38
|
+
actualValue: 'value2',
|
|
39
|
+
mutability: PropertyMutability.MUTABLE,
|
|
40
|
+
});
|
|
41
|
+
}).toThrow('expectedValue is required');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should require actualValue', () => {
|
|
45
|
+
expect(() => {
|
|
46
|
+
new PropertyMismatch({
|
|
47
|
+
propertyPath: 'Properties.Name',
|
|
48
|
+
expectedValue: 'value1',
|
|
49
|
+
mutability: PropertyMutability.MUTABLE,
|
|
50
|
+
});
|
|
51
|
+
}).toThrow('actualValue is required');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should require mutability', () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
new PropertyMismatch({
|
|
57
|
+
propertyPath: 'Properties.Name',
|
|
58
|
+
expectedValue: 'value1',
|
|
59
|
+
actualValue: 'value2',
|
|
60
|
+
});
|
|
61
|
+
}).toThrow('mutability is required');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should accept null as expectedValue', () => {
|
|
65
|
+
const mismatch = new PropertyMismatch({
|
|
66
|
+
propertyPath: 'Properties.Tags',
|
|
67
|
+
expectedValue: null,
|
|
68
|
+
actualValue: ['tag1', 'tag2'],
|
|
69
|
+
mutability: PropertyMutability.MUTABLE,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(mismatch.expectedValue).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should accept null as actualValue', () => {
|
|
76
|
+
const mismatch = new PropertyMismatch({
|
|
77
|
+
propertyPath: 'Properties.Tags',
|
|
78
|
+
expectedValue: ['tag1', 'tag2'],
|
|
79
|
+
actualValue: null,
|
|
80
|
+
mutability: PropertyMutability.MUTABLE,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(mismatch.actualValue).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('requiresReplacement', () => {
|
|
88
|
+
it('should return true for immutable property', () => {
|
|
89
|
+
const mismatch = new PropertyMismatch({
|
|
90
|
+
propertyPath: 'Properties.BucketName',
|
|
91
|
+
expectedValue: 'bucket1',
|
|
92
|
+
actualValue: 'bucket2',
|
|
93
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(mismatch.requiresReplacement()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return false for mutable property', () => {
|
|
100
|
+
const mismatch = new PropertyMismatch({
|
|
101
|
+
propertyPath: 'Properties.Tags',
|
|
102
|
+
expectedValue: ['tag1'],
|
|
103
|
+
actualValue: ['tag2'],
|
|
104
|
+
mutability: PropertyMutability.MUTABLE,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mismatch.requiresReplacement()).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return false for conditional property', () => {
|
|
111
|
+
const mismatch = new PropertyMismatch({
|
|
112
|
+
propertyPath: 'Properties.EngineVersion',
|
|
113
|
+
expectedValue: '5.7',
|
|
114
|
+
actualValue: '5.6',
|
|
115
|
+
mutability: PropertyMutability.CONDITIONAL,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(mismatch.requiresReplacement()).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('canAutoFix', () => {
|
|
123
|
+
it('should return true for mutable property', () => {
|
|
124
|
+
const mismatch = new PropertyMismatch({
|
|
125
|
+
propertyPath: 'Properties.Tags',
|
|
126
|
+
expectedValue: ['tag1'],
|
|
127
|
+
actualValue: ['tag2'],
|
|
128
|
+
mutability: PropertyMutability.MUTABLE,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(mismatch.canAutoFix()).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return false for immutable property', () => {
|
|
135
|
+
const mismatch = new PropertyMismatch({
|
|
136
|
+
propertyPath: 'Properties.BucketName',
|
|
137
|
+
expectedValue: 'bucket1',
|
|
138
|
+
actualValue: 'bucket2',
|
|
139
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(mismatch.canAutoFix()).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return false for conditional property', () => {
|
|
146
|
+
const mismatch = new PropertyMismatch({
|
|
147
|
+
propertyPath: 'Properties.EngineVersion',
|
|
148
|
+
expectedValue: '5.7',
|
|
149
|
+
actualValue: '5.6',
|
|
150
|
+
mutability: PropertyMutability.CONDITIONAL,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(mismatch.canAutoFix()).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('getSeverity', () => {
|
|
158
|
+
it('should return critical for immutable property mismatch', () => {
|
|
159
|
+
const mismatch = new PropertyMismatch({
|
|
160
|
+
propertyPath: 'Properties.BucketName',
|
|
161
|
+
expectedValue: 'bucket1',
|
|
162
|
+
actualValue: 'bucket2',
|
|
163
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(mismatch.getSeverity()).toBe('critical');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return warning for mutable property mismatch', () => {
|
|
170
|
+
const mismatch = new PropertyMismatch({
|
|
171
|
+
propertyPath: 'Properties.Tags',
|
|
172
|
+
expectedValue: ['tag1'],
|
|
173
|
+
actualValue: ['tag2'],
|
|
174
|
+
mutability: PropertyMutability.MUTABLE,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(mismatch.getSeverity()).toBe('warning');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return warning for conditional property mismatch', () => {
|
|
181
|
+
const mismatch = new PropertyMismatch({
|
|
182
|
+
propertyPath: 'Properties.EngineVersion',
|
|
183
|
+
expectedValue: '5.7',
|
|
184
|
+
actualValue: '5.6',
|
|
185
|
+
mutability: PropertyMutability.CONDITIONAL,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(mismatch.getSeverity()).toBe('warning');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('toString', () => {
|
|
193
|
+
it('should return string representation', () => {
|
|
194
|
+
const mismatch = new PropertyMismatch({
|
|
195
|
+
propertyPath: 'Properties.BucketName',
|
|
196
|
+
expectedValue: 'bucket1',
|
|
197
|
+
actualValue: 'bucket2',
|
|
198
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(mismatch.toString()).toBe(
|
|
202
|
+
'PropertyMismatch: Properties.BucketName (expected: bucket1, actual: bucket2, mutability: IMMUTABLE)'
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle null expected value', () => {
|
|
207
|
+
const mismatch = new PropertyMismatch({
|
|
208
|
+
propertyPath: 'Properties.Tags',
|
|
209
|
+
expectedValue: null,
|
|
210
|
+
actualValue: ['tag1'],
|
|
211
|
+
mutability: PropertyMutability.MUTABLE,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(mismatch.toString()).toContain('expected: null');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle null actual value', () => {
|
|
218
|
+
const mismatch = new PropertyMismatch({
|
|
219
|
+
propertyPath: 'Properties.Tags',
|
|
220
|
+
expectedValue: ['tag1'],
|
|
221
|
+
actualValue: null,
|
|
222
|
+
mutability: PropertyMutability.MUTABLE,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(mismatch.toString()).toContain('actual: null');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('toJSON', () => {
|
|
230
|
+
it('should serialize to JSON', () => {
|
|
231
|
+
const mismatch = new PropertyMismatch({
|
|
232
|
+
propertyPath: 'Properties.BucketName',
|
|
233
|
+
expectedValue: 'bucket1',
|
|
234
|
+
actualValue: 'bucket2',
|
|
235
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const json = mismatch.toJSON();
|
|
239
|
+
|
|
240
|
+
expect(json).toEqual({
|
|
241
|
+
propertyPath: 'Properties.BucketName',
|
|
242
|
+
expectedValue: 'bucket1',
|
|
243
|
+
actualValue: 'bucket2',
|
|
244
|
+
mutability: 'IMMUTABLE',
|
|
245
|
+
severity: 'critical',
|
|
246
|
+
canAutoFix: false,
|
|
247
|
+
requiresReplacement: true,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthScore Value Object
|
|
3
|
+
*
|
|
4
|
+
* Immutable health score from 0-100 with qualitative assessment
|
|
5
|
+
* - 80-100: healthy
|
|
6
|
+
* - 40-79: degraded
|
|
7
|
+
* - 0-39: unhealthy
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class HealthScore {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new HealthScore
|
|
13
|
+
*
|
|
14
|
+
* @param {number} value - Score from 0 to 100
|
|
15
|
+
*/
|
|
16
|
+
constructor(value) {
|
|
17
|
+
// Validate type
|
|
18
|
+
if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
|
|
19
|
+
throw new Error('Health score must be a number');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate range
|
|
23
|
+
if (value < 0 || value > 100) {
|
|
24
|
+
throw new Error('Health score must be between 0 and 100');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Assign property
|
|
28
|
+
this._value = value;
|
|
29
|
+
|
|
30
|
+
// Make immutable
|
|
31
|
+
Object.freeze(this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get score value
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
get value() {
|
|
39
|
+
return this._value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Prevent modification of value
|
|
44
|
+
* @throws {TypeError}
|
|
45
|
+
*/
|
|
46
|
+
set value(newValue) {
|
|
47
|
+
throw new TypeError('Cannot modify immutable property value');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get qualitative assessment
|
|
52
|
+
*
|
|
53
|
+
* @returns {'healthy' | 'degraded' | 'unhealthy'}
|
|
54
|
+
*/
|
|
55
|
+
qualitativeAssessment() {
|
|
56
|
+
if (this._value >= 80) {
|
|
57
|
+
return 'healthy';
|
|
58
|
+
} else if (this._value >= 40) {
|
|
59
|
+
return 'degraded';
|
|
60
|
+
} else {
|
|
61
|
+
return 'unhealthy';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if score is healthy (>= 80)
|
|
67
|
+
*
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
isHealthy() {
|
|
71
|
+
return this._value >= 80;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if score is degraded (40-79)
|
|
76
|
+
*
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
isDegraded() {
|
|
80
|
+
return this._value >= 40 && this._value < 80;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if score is unhealthy (< 40)
|
|
85
|
+
*
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
isUnhealthy() {
|
|
89
|
+
return this._value < 40;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get string representation
|
|
94
|
+
*
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
toString() {
|
|
98
|
+
return `${this._value} (${this.qualitativeAssessment()})`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create perfect health score (100)
|
|
103
|
+
*
|
|
104
|
+
* @returns {HealthScore}
|
|
105
|
+
*/
|
|
106
|
+
static perfect() {
|
|
107
|
+
return new HealthScore(100);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create failed health score (0)
|
|
112
|
+
*
|
|
113
|
+
* @returns {HealthScore}
|
|
114
|
+
*/
|
|
115
|
+
static failed() {
|
|
116
|
+
return new HealthScore(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create HealthScore from percentage (0.0 to 1.0)
|
|
121
|
+
*
|
|
122
|
+
* @param {number} percentage - Percentage as decimal (0.75 = 75%)
|
|
123
|
+
* @returns {HealthScore}
|
|
124
|
+
*/
|
|
125
|
+
static fromPercentage(percentage) {
|
|
126
|
+
if (typeof percentage !== 'number' || isNaN(percentage) || !isFinite(percentage)) {
|
|
127
|
+
throw new Error('Percentage must be a number');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (percentage < 0 || percentage > 1) {
|
|
131
|
+
throw new Error('Percentage must be between 0 and 1');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new HealthScore(Math.round(percentage * 100));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = HealthScore;
|