flagsmith-nodejs 6.1.0 → 7.0.2

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 (117) hide show
  1. package/.github/workflows/conventional-commit.yml +29 -0
  2. package/.github/workflows/publish.yml +20 -17
  3. package/.github/workflows/pull_request.yaml +36 -33
  4. package/.github/workflows/release-please.yml +18 -0
  5. package/.gitmodules +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.cjs +9 -1
  9. package/.release-please-manifest.json +1 -0
  10. package/CHANGELOG.md +592 -0
  11. package/CODEOWNERS +1 -0
  12. package/README.md +0 -2
  13. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  14. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  15. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  19. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  20. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  21. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  22. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  23. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  24. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  25. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  26. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  27. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  28. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  29. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  30. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  31. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  32. package/build/cjs/flagsmith-engine/index.js +176 -56
  33. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  34. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  35. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  36. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  37. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  38. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  39. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  40. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  41. package/build/cjs/sdk/analytics.js +3 -1
  42. package/build/cjs/sdk/index.d.ts +1 -3
  43. package/build/cjs/sdk/index.js +63 -24
  44. package/build/cjs/sdk/models.d.ts +8 -1
  45. package/build/cjs/sdk/models.js +29 -1
  46. package/build/cjs/sdk/utils.d.ts +1 -0
  47. package/build/cjs/sdk/utils.js +14 -1
  48. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  49. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  50. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  51. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  52. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  53. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  54. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  55. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  56. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  57. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  58. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  59. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  60. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  61. package/build/esm/flagsmith-engine/features/models.js +2 -2
  62. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  63. package/build/esm/flagsmith-engine/features/types.js +6 -0
  64. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  65. package/build/esm/flagsmith-engine/features/util.js +3 -0
  66. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  67. package/build/esm/flagsmith-engine/index.js +161 -43
  68. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  69. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  70. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  71. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  72. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  73. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  74. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  75. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  76. package/build/esm/sdk/analytics.js +3 -1
  77. package/build/esm/sdk/index.d.ts +1 -3
  78. package/build/esm/sdk/index.js +63 -24
  79. package/build/esm/sdk/models.d.ts +8 -1
  80. package/build/esm/sdk/models.js +29 -1
  81. package/build/esm/sdk/utils.d.ts +1 -0
  82. package/build/esm/sdk/utils.js +12 -0
  83. package/flagsmith-engine/environments/models.ts +3 -1
  84. package/flagsmith-engine/environments/util.ts +2 -1
  85. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  86. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  87. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  88. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  89. package/flagsmith-engine/evaluation/models.ts +96 -0
  90. package/flagsmith-engine/features/models.ts +3 -2
  91. package/flagsmith-engine/features/types.ts +5 -0
  92. package/flagsmith-engine/features/util.ts +4 -0
  93. package/flagsmith-engine/index.ts +229 -72
  94. package/flagsmith-engine/segments/constants.ts +1 -0
  95. package/flagsmith-engine/segments/evaluators.ts +178 -62
  96. package/flagsmith-engine/segments/models.ts +171 -23
  97. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  98. package/package.json +13 -2
  99. package/release-please-config.json +62 -0
  100. package/sdk/analytics.ts +3 -1
  101. package/sdk/index.ts +89 -30
  102. package/sdk/models.ts +44 -2
  103. package/sdk/utils.ts +13 -0
  104. package/tests/engine/e2e/engine.test.ts +43 -38
  105. package/tests/engine/unit/engine.test.ts +306 -60
  106. package/tests/engine/unit/mappers.test.ts +353 -0
  107. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  108. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  109. package/tests/engine/unit/utils/utils.test.ts +7 -7
  110. package/tests/engine/unit/utils.ts +1 -1
  111. package/tests/sdk/analytics.test.ts +6 -1
  112. package/tests/sdk/data/environment.json +1 -0
  113. package/tests/sdk/flagsmith-environment-flags.test.ts +28 -0
  114. package/tests/sdk/flagsmith-identity-flags.test.ts +11 -2
  115. package/tests/sdk/flagsmith.test.ts +190 -3
  116. package/tests/sdk/offline-handlers.test.ts +3 -1
  117. 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,102 +13,346 @@ import {
11
13
  environment,
12
14
  environmentWithSegmentOverride,
13
15
  feature1,
14
- getEnvironmentFeatureStateForFeature,
15
- getEnvironmentFeatureStateForFeatureByName,
16
16
  identity,
17
17
  identityInSegment,
18
18
  segmentConditionProperty,
19
19
  segmentConditionStringValue
20
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';
21
25
 
22
- test('test_identity_get_feature_state_without_any_override', () => {
23
- 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);
24
29
 
25
- 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);
26
34
  });
27
35
 
28
- test('test_identity_get_feature_state_without_any_override_no_fs', () => {
29
- expect(() => {
30
- getIdentityFeatureState(environment(), identity(), 'nonExistentName');
31
- }).toThrowError('Feature State Not Found');
32
- });
33
-
34
- test('test_identity_get_all_feature_states_no_segments', () => {
36
+ test('test_get_evaluation_result_with_identity_override_and_no_segment_override', () => {
35
37
  const env = environment();
36
38
  const ident = identity();
37
39
  const overridden_feature = new FeatureModel(3, 'overridden_feature', CONSTANTS.STANDARD);
38
40
 
39
41
  env.featureStates.push(new FeatureStateModel(overridden_feature, false, 3));
40
-
41
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);
42
47
 
43
- const featureStates = getIdentityFeatureStates(env, ident);
48
+ expect(Object.keys(result.flags).length).toBe(3);
44
49
 
45
- expect(featureStates.length).toBe(3);
46
- for (const featuresState of featureStates) {
47
- const environmentFeatureState = getEnvironmentFeatureStateForFeature(
48
- env,
49
- 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
50
62
  );
51
- const expected =
52
- environmentFeatureState?.feature == overridden_feature
53
- ? true
54
- : environmentFeatureState?.enabled;
55
- expect(featuresState.enabled).toBe(expected);
56
63
  }
57
64
  });
58
65
 
59
66
  test('test_identity_get_all_feature_states_with_traits', () => {
60
67
  const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue);
61
68
 
62
- const featureStates = getIdentityFeatureStates(
63
- environmentWithSegmentOverride(),
64
- identityInSegment(),
65
- [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`
66
80
  );
67
- expect(featureStates[0].getValue()).toBe('segment_override');
68
81
  });
69
82
 
70
- test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => {
71
- 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);
72
87
 
73
- const env = environmentWithSegmentOverride();
74
- env.project.hideDisabledFlags = true;
88
+ expect(Object.keys(result.flags).length).toBe(Object.keys(context.features || {}).length);
75
89
 
76
- const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]);
77
- 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
+ }
78
99
  });
79
100
 
80
- test('test_environment_get_all_feature_states', () => {
81
- const env = environment();
82
- 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
+ });
83
108
 
84
- 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);
85
126
  });
86
127
 
87
- test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => {
88
- 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
+ };
89
192
 
90
- env.project.hideDisabledFlags = true;
193
+ const result = evaluateSegments(context as any);
91
194
 
92
- 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
+ );
93
202
 
94
- expect(featureStates).not.toBe(env.featureStates);
95
- for (const fs of featureStates) {
96
- expect(fs.enabled).toBe(true);
97
- }
203
+ expect(Object.keys(result.segmentOverrides)).toEqual(['feature1']);
204
+ expect(result.segmentOverrides.feature1.segmentName).toBe('segment_with_overrides');
98
205
  });
99
206
 
100
- test('test_environment_get_feature_state', () => {
101
- const env = environment();
102
- const feature = feature1();
103
- 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
+ };
104
279
 
105
- 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);
106
287
  });
107
288
 
108
- test('test_environment_get_feature_state_raises_feature_state_not_found', () => {
109
- expect(() => {
110
- getEnvironmentFeatureState(environment(), 'not_a_feature_name');
111
- }).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');
112
358
  });