flagsmith-nodejs 6.2.0 → 7.0.3

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 (100) hide show
  1. package/.github/workflows/publish.yml +8 -5
  2. package/.github/workflows/pull_request.yaml +3 -0
  3. package/.gitmodules +1 -1
  4. package/.husky/pre-commit +1 -0
  5. package/.nvmrc +1 -0
  6. package/.release-please-manifest.json +1 -1
  7. package/CHANGELOG.md +49 -0
  8. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  9. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  10. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  11. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  12. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  13. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  14. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  15. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  19. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  20. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  21. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  22. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  23. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  24. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  25. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  26. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  27. package/build/cjs/flagsmith-engine/index.js +176 -56
  28. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  29. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  30. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  31. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  32. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  33. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  34. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  35. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  36. package/build/cjs/sdk/index.d.ts +1 -3
  37. package/build/cjs/sdk/index.js +22 -19
  38. package/build/cjs/sdk/models.d.ts +8 -1
  39. package/build/cjs/sdk/models.js +29 -1
  40. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  41. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  42. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  43. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  44. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  45. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  46. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  47. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  48. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  49. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  50. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  51. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  52. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  53. package/build/esm/flagsmith-engine/features/models.js +2 -2
  54. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  55. package/build/esm/flagsmith-engine/features/types.js +6 -0
  56. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  57. package/build/esm/flagsmith-engine/features/util.js +3 -0
  58. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  59. package/build/esm/flagsmith-engine/index.js +161 -43
  60. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  61. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  62. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  63. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  64. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  65. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  66. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  67. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  68. package/build/esm/sdk/index.d.ts +1 -3
  69. package/build/esm/sdk/index.js +21 -18
  70. package/build/esm/sdk/models.d.ts +8 -1
  71. package/build/esm/sdk/models.js +29 -1
  72. package/flagsmith-engine/environments/models.ts +3 -1
  73. package/flagsmith-engine/environments/util.ts +2 -1
  74. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  75. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  76. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  77. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  78. package/flagsmith-engine/evaluation/models.ts +96 -0
  79. package/flagsmith-engine/features/models.ts +3 -2
  80. package/flagsmith-engine/features/types.ts +5 -0
  81. package/flagsmith-engine/features/util.ts +4 -0
  82. package/flagsmith-engine/index.ts +229 -72
  83. package/flagsmith-engine/segments/constants.ts +1 -0
  84. package/flagsmith-engine/segments/evaluators.ts +178 -62
  85. package/flagsmith-engine/segments/models.ts +171 -23
  86. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  87. package/package.json +13 -2
  88. package/sdk/index.ts +36 -23
  89. package/sdk/models.ts +44 -2
  90. package/tests/engine/e2e/engine.test.ts +43 -38
  91. package/tests/engine/unit/engine.test.ts +306 -59
  92. package/tests/engine/unit/mappers.test.ts +353 -0
  93. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  94. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  95. package/tests/engine/unit/utils/utils.test.ts +7 -7
  96. package/tests/engine/unit/utils.ts +1 -1
  97. package/tests/sdk/data/environment.json +1 -0
  98. package/tests/sdk/flagsmith.test.ts +29 -3
  99. package/tests/sdk/offline-handlers.test.ts +3 -1
  100. package/vitest.config.esm.ts +34 -0
@@ -1,8 +1,10 @@
1
1
  import {
2
- getEnvironmentFeatureState,
3
- getEnvironmentFeatureStates,
4
- getIdentityFeatureState,
5
- getIdentityFeatureStates
2
+ evaluateFeatures,
3
+ evaluateSegments,
4
+ getEvaluationResult,
5
+ isHigherPriority,
6
+ SegmentOverrides,
7
+ shouldApplyOverride
6
8
  } from '../../../flagsmith-engine/index.js';
7
9
  import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
8
10
  import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
@@ -11,101 +13,346 @@ import {
11
13
  environment,
12
14
  environmentWithSegmentOverride,
13
15
  feature1,
14
- getEnvironmentFeatureStateForFeature,
15
16
  identity,
16
17
  identityInSegment,
17
18
  segmentConditionProperty,
18
19
  segmentConditionStringValue
19
20
  } from './utils.js';
21
+ import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
22
+ import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js';
23
+ import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js';
24
+ import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
20
25
 
21
- test('test_identity_get_feature_state_without_any_override', () => {
22
- const feature_state = getIdentityFeatureState(environment(), identity(), feature1().name);
26
+ test('test_get_evaluation_result_without_any_override', () => {
27
+ const context = getEvaluationContext(environment(), identity());
28
+ const result = getEvaluationResult(context);
23
29
 
24
- expect(feature_state.feature).toStrictEqual(feature1());
30
+ const flag = Object.values(result.flags).find(f => f.name === feature1().name);
31
+ expect(flag).toBeDefined();
32
+ expect(flag?.name).toBe(feature1().name);
33
+ expect(flag?.reason).toBe(TARGETING_REASONS.DEFAULT);
25
34
  });
26
35
 
27
- test('test_identity_get_feature_state_without_any_override_no_fs', () => {
28
- expect(() => {
29
- getIdentityFeatureState(environment(), identity(), 'nonExistentName');
30
- }).toThrowError('Feature State Not Found');
31
- });
32
-
33
- test('test_identity_get_all_feature_states_no_segments', () => {
36
+ test('test_get_evaluation_result_with_identity_override_and_no_segment_override', () => {
34
37
  const env = environment();
35
38
  const ident = identity();
36
39
  const overridden_feature = new FeatureModel(3, 'overridden_feature', CONSTANTS.STANDARD);
37
40
 
38
41
  env.featureStates.push(new FeatureStateModel(overridden_feature, false, 3));
39
-
40
42
  ident.identityFeatures = [new FeatureStateModel(overridden_feature, true, 4)];
43
+ env.identityOverrides = [ident];
44
+
45
+ const context = getEvaluationContext(env, ident);
46
+ const result = getEvaluationResult(context);
41
47
 
42
- const featureStates = getIdentityFeatureStates(env, ident);
48
+ expect(Object.keys(result.flags).length).toBe(3);
43
49
 
44
- expect(featureStates.length).toBe(3);
45
- for (const featuresState of featureStates) {
46
- const environmentFeatureState = getEnvironmentFeatureStateForFeature(
47
- env,
48
- featuresState.feature
50
+ for (const flag of Object.values(result.flags)) {
51
+ const environmentFeature = Object.values(context.features || {}).find(
52
+ f => f.name === flag.name
53
+ );
54
+
55
+ const expected = flag.name === 'overridden_feature' ? true : environmentFeature?.enabled;
56
+
57
+ expect(flag.enabled).toBe(expected);
58
+ expect(flag.reason).toBe(
59
+ flag.name === 'overridden_feature'
60
+ ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${IDENTITY_OVERRIDE_SEGMENT_NAME}`
61
+ : TARGETING_REASONS.DEFAULT
49
62
  );
50
- const expected =
51
- environmentFeatureState?.feature == overridden_feature
52
- ? true
53
- : environmentFeatureState?.enabled;
54
- expect(featuresState.enabled).toBe(expected);
55
63
  }
56
64
  });
57
65
 
58
66
  test('test_identity_get_all_feature_states_with_traits', () => {
59
67
  const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue);
60
68
 
61
- const featureStates = getIdentityFeatureStates(
62
- environmentWithSegmentOverride(),
63
- identityInSegment(),
64
- [trait_models]
69
+ const context = getEvaluationContext(environmentWithSegmentOverride(), identityInSegment(), [
70
+ trait_models
71
+ ]);
72
+
73
+ const result = getEvaluationResult(context);
74
+
75
+ const overriddenFlag = Object.values(result.flags).find(f => f.value === 'segment_override');
76
+ expect(overriddenFlag).toBeDefined();
77
+ expect(overriddenFlag?.value).toBe('segment_override');
78
+ expect(overriddenFlag?.reason).toEqual(
79
+ `${TARGETING_REASONS.TARGETING_MATCH}; segment=test name`
65
80
  );
66
- expect(featureStates[0].getValue()).toBe('segment_override');
67
81
  });
68
82
 
69
- test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => {
70
- const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue);
83
+ test('test_environment_get_all_feature_states', () => {
84
+ const env = environment();
85
+ const context = getEvaluationContext(env);
86
+ const result = getEvaluationResult(context);
71
87
 
72
- const env = environmentWithSegmentOverride();
73
- env.project.hideDisabledFlags = true;
88
+ expect(Object.keys(result.flags).length).toBe(Object.keys(context.features || {}).length);
74
89
 
75
- const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]);
76
- expect(featureStates.length).toBe(0);
90
+ Object.values(result.flags).forEach(flag => {
91
+ expect(flag.reason).toBe(TARGETING_REASONS.DEFAULT);
92
+ });
93
+
94
+ for (const flag of Object.values(result.flags)) {
95
+ const envFeature = Object.values(context.features || {}).find(f => f.name === flag.name);
96
+ expect(flag.enabled).toBe(envFeature?.enabled);
97
+ expect(flag.value).toBe(envFeature?.value);
98
+ }
77
99
  });
78
100
 
79
- test('test_environment_get_all_feature_states', () => {
80
- const env = environment();
81
- const featureStates = getEnvironmentFeatureStates(env);
101
+ test('isHigherPriority should handle undefined priorities correctly', () => {
102
+ expect(isHigherPriority(1, 2)).toBe(true);
103
+ expect(isHigherPriority(2, 1)).toBe(false);
104
+ expect(isHigherPriority(undefined, 5)).toBe(false);
105
+ expect(isHigherPriority(5, undefined)).toBe(true);
106
+ expect(isHigherPriority(undefined, undefined)).toBe(false);
107
+ });
82
108
 
83
- expect(featureStates).toBe(env.featureStates);
109
+ test('shouldApplyOverride with priority conflicts', () => {
110
+ const existingOverrides: SegmentOverrides = {
111
+ feature1: {
112
+ feature: {
113
+ key: 'key',
114
+ name: 'feature1',
115
+ enabled: true,
116
+ value: 'value',
117
+ priority: 5,
118
+ metadata: { id: 1 }
119
+ },
120
+ segmentName: 'segment1'
121
+ }
122
+ };
123
+
124
+ expect(shouldApplyOverride({ name: 'feature1', priority: 2 }, existingOverrides)).toBe(true);
125
+ expect(shouldApplyOverride({ name: 'feature1', priority: 10 }, existingOverrides)).toBe(false);
84
126
  });
85
127
 
86
- test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => {
87
- const env = environment();
128
+ test('evaluateSegments handles segments with identity identifier matching', () => {
129
+ const context: EvaluationContext = {
130
+ environment: {
131
+ key: 'test-env',
132
+ name: 'Test Environment'
133
+ },
134
+ identity: {
135
+ key: 'test-user',
136
+ identifier: 'test-user'
137
+ },
138
+ segments: {
139
+ '1': {
140
+ key: '1',
141
+ name: 'segment_with_no_overrides',
142
+ rules: [
143
+ {
144
+ type: 'ALL',
145
+ conditions: [
146
+ {
147
+ property: '$.identity.identifier',
148
+ operator: 'EQUAL',
149
+ value: 'test-user'
150
+ }
151
+ ]
152
+ }
153
+ ],
154
+ overrides: []
155
+ },
156
+ '2': {
157
+ key: '2',
158
+ name: 'segment_with_overrides',
159
+ rules: [
160
+ {
161
+ type: 'ALL',
162
+ conditions: [
163
+ {
164
+ property: '$.identity.identifier',
165
+ operator: 'EQUAL',
166
+ value: 'test-user'
167
+ }
168
+ ]
169
+ }
170
+ ],
171
+ overrides: [
172
+ {
173
+ key: 'override1',
174
+ name: 'feature1',
175
+ enabled: true,
176
+ value: 'overridden_value',
177
+ priority: 1
178
+ }
179
+ ]
180
+ }
181
+ },
182
+ features: {
183
+ feature1: {
184
+ key: 'fs1',
185
+ name: 'feature1',
186
+ enabled: false,
187
+ value: 'default_value',
188
+ metadata: { id: 1 }
189
+ }
190
+ }
191
+ };
88
192
 
89
- env.project.hideDisabledFlags = true;
193
+ const result = evaluateSegments(context as any);
90
194
 
91
- const featureStates = getEnvironmentFeatureStates(env);
195
+ expect(result.segments).toHaveLength(2);
196
+ expect(result.segments).toEqual(
197
+ expect.arrayContaining([
198
+ { name: 'segment_with_no_overrides' },
199
+ { name: 'segment_with_overrides' }
200
+ ])
201
+ );
92
202
 
93
- expect(featureStates).not.toBe(env.featureStates);
94
- for (const fs of featureStates) {
95
- expect(fs.enabled).toBe(true);
96
- }
203
+ expect(Object.keys(result.segmentOverrides)).toEqual(['feature1']);
204
+ expect(result.segmentOverrides.feature1.segmentName).toBe('segment_with_overrides');
97
205
  });
98
206
 
99
- test('test_environment_get_feature_state', () => {
100
- const env = environment();
101
- const feature = feature1();
102
- const featureState = getEnvironmentFeatureState(env, feature.name);
207
+ test('evaluateSegments handles priority conflicts correctly', () => {
208
+ const context: EvaluationContext = {
209
+ environment: {
210
+ key: 'test-env',
211
+ name: 'Test Environment'
212
+ },
213
+ identity: {
214
+ key: 'test-user',
215
+ identifier: 'test-user'
216
+ },
217
+ segments: {
218
+ '1': {
219
+ key: '1',
220
+ name: 'low_priority_segment',
221
+ rules: [
222
+ {
223
+ type: 'ALL',
224
+ conditions: [
225
+ {
226
+ property: '$.identity.identifier',
227
+ operator: 'EQUAL',
228
+ value: 'test-user'
229
+ }
230
+ ]
231
+ }
232
+ ],
233
+ overrides: [
234
+ {
235
+ key: 'override1',
236
+ name: 'feature1',
237
+ enabled: true,
238
+ value: 'low_priority_value',
239
+ priority: 10
240
+ }
241
+ ]
242
+ },
243
+ '2': {
244
+ key: '2',
245
+ name: 'high_priority_segment',
246
+ rules: [
247
+ {
248
+ type: 'ALL',
249
+ conditions: [
250
+ {
251
+ property: '$.identity.identifier',
252
+ operator: 'EQUAL',
253
+ value: 'test-user'
254
+ }
255
+ ]
256
+ }
257
+ ],
258
+ overrides: [
259
+ {
260
+ key: 'override2',
261
+ name: 'feature1',
262
+ enabled: false,
263
+ value: 'high_priority_value',
264
+ priority: 1
265
+ }
266
+ ]
267
+ }
268
+ },
269
+ features: {
270
+ feature1: {
271
+ key: 'fs1',
272
+ name: 'feature1',
273
+ enabled: false,
274
+ value: 'default_value',
275
+ metadata: { id: 1 }
276
+ }
277
+ }
278
+ };
103
279
 
104
- expect(featureState.feature).toStrictEqual(feature);
280
+ const result = evaluateSegments(context as any);
281
+
282
+ expect(result.segments).toHaveLength(2);
283
+
284
+ expect(result.segmentOverrides.feature1.segmentName).toBe('high_priority_segment');
285
+ expect(result.segmentOverrides.feature1.feature.value).toBe('high_priority_value');
286
+ expect(result.segmentOverrides.feature1.feature.priority).toBe(1);
105
287
  });
106
288
 
107
- test('test_environment_get_feature_state_raises_feature_state_not_found', () => {
108
- expect(() => {
109
- getEnvironmentFeatureState(environment(), 'not_a_feature_name');
110
- }).toThrowError('Feature State Not Found');
289
+ test('evaluateSegments with non-matching identity returns empty', () => {
290
+ const context: EvaluationContext = {
291
+ environment: {
292
+ key: 'test-env',
293
+ name: 'Test Environment'
294
+ },
295
+ identity: {
296
+ key: 'test-user',
297
+ identifier: 'test-user'
298
+ },
299
+ segments: {
300
+ '1': {
301
+ key: '1',
302
+ name: 'segment_for_specific_user',
303
+ rules: [
304
+ {
305
+ type: 'ALL',
306
+ conditions: [
307
+ {
308
+ property: '$.identity.identifier',
309
+ operator: 'EQUAL',
310
+ value: 'test-user-123'
311
+ }
312
+ ]
313
+ }
314
+ ],
315
+ overrides: [
316
+ {
317
+ key: 'override1',
318
+ name: 'feature1',
319
+ enabled: true,
320
+ value: 'overridden_value'
321
+ }
322
+ ]
323
+ }
324
+ },
325
+ features: {}
326
+ };
327
+
328
+ const result = evaluateSegments(context as any);
329
+
330
+ expect(result.segments).toEqual([]);
331
+ expect(result.segmentOverrides).toEqual({});
332
+ });
333
+
334
+ test('evaluateFeatures with multivariate evaluation', () => {
335
+ const context = {
336
+ features: {
337
+ mv_feature: {
338
+ key: 'mv',
339
+ name: 'Multivariate Feature',
340
+ enabled: true,
341
+ value: 'default',
342
+ variants: [
343
+ { value: 'variant_a', weight: 0, priority: 1 },
344
+ { value: 'variant_b', weight: 100, priority: 2 }
345
+ ],
346
+ metadata: { id: 1 }
347
+ }
348
+ },
349
+ identity: { key: 'test_user', identifier: 'test_user' },
350
+ environment: {
351
+ key: 'test_env',
352
+ name: 'Test Environment'
353
+ }
354
+ };
355
+
356
+ const flags = evaluateFeatures(context as any, {});
357
+ expect(flags['Multivariate Feature'].value).toBe('variant_b');
111
358
  });