flagsmith-nodejs 6.2.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 (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 +40 -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
@@ -0,0 +1,353 @@
1
+ import { test, expect, describe } from 'vitest';
2
+ import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
3
+ import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js';
4
+ import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js';
5
+ import { IdentityModel } from '../../../flagsmith-engine/identities/models.js';
6
+ import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js';
7
+ import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
8
+ import {
9
+ MultivariateFeatureOptionModel,
10
+ MultivariateFeatureStateValueModel
11
+ } from '../../../flagsmith-engine/features/models.js';
12
+ import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
13
+ import { readFileSync } from 'fs';
14
+ import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
15
+ import { SegmentSource } from '../../../flagsmith-engine/evaluation/models.js';
16
+
17
+ const DATA_DIR = __dirname + '/../../sdk/data/';
18
+
19
+ describe('getEvaluationContext', () => {
20
+ const environmentJSON = JSON.parse(readFileSync(DATA_DIR + 'environment.json', 'utf-8'));
21
+ const testEnvironment = buildEnvironmentModel(environmentJSON);
22
+
23
+ test('produces evaluation context from environment document', () => {
24
+ // When
25
+ const context = getEvaluationContext(testEnvironment);
26
+
27
+ // Then - verify environment
28
+ expect(context).toBeDefined();
29
+ expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
30
+ expect(context.environment?.name).toBe('Test environment');
31
+ expect(context.identity).toBeUndefined();
32
+
33
+ // Verify segments
34
+ expect(context.segments).toBeDefined();
35
+ expect(context.segments).toHaveProperty('1');
36
+
37
+ const segment = context.segments!['1'];
38
+ expect(segment.key).toBe('1');
39
+ expect(segment.name).toBe('regular_segment');
40
+ expect(segment.rules.length).toBe(1);
41
+ expect(segment.overrides).toBeDefined();
42
+ expect(Array.isArray(segment.overrides)).toBe(true);
43
+ expect(segment.metadata?.source).toBe(SegmentSource.API);
44
+ expect(segment.metadata?.id).toBe(1);
45
+
46
+ // Verify segment rules
47
+ expect(segment.rules[0].type).toBe('ALL');
48
+ expect(segment.rules[0].conditions).toEqual([]);
49
+ expect(segment.rules[0].rules?.length).toBe(1);
50
+
51
+ const nestedRule = segment.rules[0].rules?.[0]!;
52
+ expect(nestedRule.type).toBe('ANY');
53
+ expect(nestedRule.conditions?.length).toBe(1);
54
+ expect(nestedRule.rules?.length).toEqual(0);
55
+
56
+ const condition = nestedRule.conditions?.[0]!;
57
+ expect(condition.property).toBe('age');
58
+ expect(condition.operator).toBe('LESS_THAN');
59
+ expect(condition.value).toBe('40');
60
+
61
+ // Verify identity override segment
62
+ const identityOverrideSegment = Object.values(context.segments!).find(
63
+ s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
64
+ );
65
+ expect(identityOverrideSegment).toBeDefined();
66
+ expect(identityOverrideSegment!.name).toBe(IDENTITY_OVERRIDE_SEGMENT_NAME);
67
+ expect(identityOverrideSegment!.rules.length).toBe(1);
68
+ expect(identityOverrideSegment!.overrides?.length).toBe(1);
69
+
70
+ const overrideRule = identityOverrideSegment!.rules?.[0]!;
71
+ expect(overrideRule.type).toBe('ALL');
72
+ expect(overrideRule.conditions?.length).toBe(1);
73
+
74
+ const overrideCondition = overrideRule.conditions?.[0]!;
75
+ expect(overrideCondition.property).toBe('$.identity.identifier');
76
+ expect(overrideCondition.operator).toBe('IN');
77
+ expect(overrideCondition.value).toContain('overridden-id');
78
+
79
+ const override = identityOverrideSegment!.overrides?.[0]!;
80
+ expect(override.name).toBe('some_feature');
81
+ expect(override.enabled).toBe(false);
82
+ expect(override.value).toBe('some-overridden-value');
83
+ expect(override.priority).toBe(-Infinity);
84
+ expect(override.metadata?.id).toBe(1);
85
+
86
+ // Verify features
87
+ expect(context.features).toBeDefined();
88
+ expect(context.features).toHaveProperty('some_feature');
89
+
90
+ const someFeature = context.features!['some_feature'];
91
+ expect(someFeature.name).toBe('some_feature');
92
+ expect(someFeature.enabled).toBe(true);
93
+ expect(someFeature.value).toBe('some-value');
94
+ expect(someFeature.priority).toBeUndefined();
95
+ expect(someFeature.metadata?.id).toBe(1);
96
+
97
+ // Verify multivariate feature
98
+ expect(context.features).toHaveProperty('mv_feature');
99
+ const mvFeature = context.features!['mv_feature'];
100
+ expect(mvFeature.name).toBe('mv_feature');
101
+ expect(mvFeature.enabled).toBe(false);
102
+ expect(mvFeature.value).toBe('foo');
103
+ expect(mvFeature.priority).toBeUndefined();
104
+ expect(mvFeature.variants?.length).toBe(1);
105
+
106
+ const variant = mvFeature.variants![0];
107
+ expect(variant.value).toBe('bar');
108
+ expect(variant.weight).toBe(100);
109
+ expect(variant.priority).toBe(1);
110
+ });
111
+
112
+ test('maps multivariate features with multiple variants correctly', () => {
113
+ // Given
114
+ const mvOption1 = new MultivariateFeatureOptionModel('variant_a', 100);
115
+ const mvOption2 = new MultivariateFeatureOptionModel('variant_b', 200);
116
+ const mvOption3 = new MultivariateFeatureOptionModel('variant_c', 150);
117
+
118
+ const mvValue1 = new MultivariateFeatureStateValueModel(
119
+ mvOption1,
120
+ 30,
121
+ 100,
122
+ '00000000-0000-0000-0000-000000000001'
123
+ );
124
+
125
+ const mvValue2 = new MultivariateFeatureStateValueModel(
126
+ mvOption2,
127
+ 50,
128
+ 200,
129
+ '00000000-0000-0000-0000-000000000002'
130
+ );
131
+
132
+ const mvValue3 = new MultivariateFeatureStateValueModel(
133
+ mvOption3,
134
+ 20,
135
+ 150,
136
+ '00000000-0000-0000-0000-000000000003'
137
+ );
138
+
139
+ const feature = new FeatureModel(999, 'multi_variant_feature', CONSTANTS.MULTIVARIATE);
140
+ const featureState = new FeatureStateModel(feature, true, 999);
141
+ featureState.setValue('control');
142
+ featureState.multivariateFeatureStateValues = [mvValue1, mvValue2, mvValue3];
143
+
144
+ const envWithMv = new EnvironmentModel(1, 'test_key', testEnvironment.project, 'Test Env');
145
+ envWithMv.featureStates = [featureState];
146
+
147
+ // When
148
+ const context = getEvaluationContext(envWithMv);
149
+
150
+ // Then
151
+ const featureContext = context.features!['multi_variant_feature'];
152
+ expect(featureContext.variants?.length).toBe(3);
153
+
154
+ expect(featureContext.variants![0].value).toBe('variant_a');
155
+ expect(featureContext.variants![0].weight).toBe(30);
156
+ expect(featureContext.variants![0].priority).toBe(100);
157
+
158
+ expect(featureContext.variants![1].value).toBe('variant_b');
159
+ expect(featureContext.variants![1].weight).toBe(50);
160
+ expect(featureContext.variants![1].priority).toBe(200);
161
+
162
+ expect(featureContext.variants![2].value).toBe('variant_c');
163
+ expect(featureContext.variants![2].weight).toBe(20);
164
+ expect(featureContext.variants![2].priority).toBe(150);
165
+ });
166
+
167
+ test('handles multivariate features without IDs using UUID', () => {
168
+ // Given
169
+ const mvOption1 = new MultivariateFeatureOptionModel('option_x', undefined);
170
+ const mvOption2 = new MultivariateFeatureOptionModel('option_y', undefined);
171
+
172
+ const mvValue1 = new MultivariateFeatureStateValueModel(
173
+ mvOption1,
174
+ 60,
175
+ undefined as any,
176
+ 'aaaaaaaa-bbbb-cccc-dddd-000000000001'
177
+ );
178
+
179
+ const mvValue2 = new MultivariateFeatureStateValueModel(
180
+ mvOption2,
181
+ 40,
182
+ undefined as any,
183
+ 'aaaaaaaa-bbbb-cccc-dddd-000000000002'
184
+ );
185
+
186
+ const feature = new FeatureModel(888, 'uuid_variant_feature', CONSTANTS.MULTIVARIATE);
187
+ const featureState = new FeatureStateModel(feature, true, 888);
188
+ featureState.setValue('default');
189
+ featureState.multivariateFeatureStateValues = [mvValue1, mvValue2];
190
+
191
+ const envWithUuid = new EnvironmentModel(
192
+ 1,
193
+ 'test_key',
194
+ testEnvironment.project,
195
+ 'Test Env'
196
+ );
197
+ envWithUuid.featureStates = [featureState];
198
+
199
+ // When
200
+ const context = getEvaluationContext(envWithUuid);
201
+
202
+ // Then
203
+ const featureContext = context.features!['uuid_variant_feature'];
204
+ expect(featureContext.variants?.length).toBe(2);
205
+
206
+ // When using UUID-based priorities, they become bigints
207
+ expect(
208
+ typeof featureContext.variants![0].priority === 'number' ||
209
+ typeof featureContext.variants![0].priority === 'bigint'
210
+ ).toBe(true);
211
+ expect(
212
+ typeof featureContext.variants![1].priority === 'number' ||
213
+ typeof featureContext.variants![1].priority === 'bigint'
214
+ ).toBe(true);
215
+ expect(featureContext.variants![0].priority).not.toBe(featureContext.variants![1].priority);
216
+ });
217
+
218
+ test('handles environment with no features', () => {
219
+ // Given - create a copy with no features
220
+ const emptyEnvJSON = { ...environmentJSON, feature_states: [] };
221
+ const emptyEnv = buildEnvironmentModel(emptyEnvJSON);
222
+
223
+ // When
224
+ const context = getEvaluationContext(emptyEnv);
225
+
226
+ // Then
227
+ expect(context.features).toEqual({});
228
+ expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
229
+ expect(context.environment?.name).toBe('Test environment');
230
+ });
231
+
232
+ test('produces evaluation context with identity', () => {
233
+ // Given
234
+ const identity = new IdentityModel(
235
+ '2024-01-01T00:00:00Z',
236
+ [new TraitModel('email', 'test@example.com'), new TraitModel('age', 25)],
237
+ [],
238
+ 'B62qaMZNwfiqT76p38ggrQ',
239
+ 'test_user'
240
+ );
241
+
242
+ // When
243
+ const context = getEvaluationContext(testEnvironment, identity);
244
+
245
+ // Then
246
+ expect(context.identity).toBeDefined();
247
+ expect(context.identity?.identifier).toBe('test_user');
248
+ expect(context.identity?.traits).toEqual({
249
+ email: 'test@example.com',
250
+ age: 25
251
+ });
252
+ });
253
+
254
+ test('produces evaluation context with override traits', () => {
255
+ // Given
256
+ const identity = new IdentityModel(
257
+ '2024-01-01T00:00:00Z',
258
+ [new TraitModel('email', 'original@example.com')],
259
+ [],
260
+ 'B62qaMZNwfiqT76p38ggrQ',
261
+ 'test_user',
262
+ undefined,
263
+ 456
264
+ );
265
+
266
+ const overrideTraits = [
267
+ new TraitModel('email', 'override@example.com'),
268
+ new TraitModel('premium', true)
269
+ ];
270
+
271
+ // When
272
+ const context = getEvaluationContext(testEnvironment, identity, overrideTraits);
273
+
274
+ // Then
275
+ expect(context.identity?.traits).toEqual({
276
+ email: 'override@example.com',
277
+ premium: true
278
+ });
279
+ });
280
+
281
+ test('produces evaluation context without identity when isEnvironmentEvaluation is true', () => {
282
+ // Given
283
+ const identity = new IdentityModel(
284
+ '2024-01-01T00:00:00Z',
285
+ [new TraitModel('test', 'value')],
286
+ [],
287
+ 'B62qaMZNwfiqT76p38ggrQ',
288
+ 'test_user',
289
+ undefined,
290
+ 789
291
+ );
292
+
293
+ // When
294
+ const context = getEvaluationContext(testEnvironment, identity, undefined, true);
295
+
296
+ // Then
297
+ expect(context.identity).toBeUndefined();
298
+ expect(context.environment).toBeDefined();
299
+ expect(context.features).toBeDefined();
300
+ expect(context.segments).toBeDefined();
301
+ });
302
+
303
+ test('handles identity without django_id', () => {
304
+ // Given
305
+ const identity = new IdentityModel(
306
+ '2024-01-01T00:00:00Z',
307
+ [new TraitModel('name', 'John')],
308
+ [],
309
+ 'B62qaMZNwfiqT76p38ggrQ',
310
+ 'john_doe',
311
+ undefined,
312
+ undefined
313
+ );
314
+
315
+ // When
316
+ const context = getEvaluationContext(testEnvironment, identity);
317
+
318
+ // Then
319
+ expect(context.identity?.identifier).toBe('john_doe');
320
+ expect(context.identity?.key).toBeUndefined();
321
+ expect(context.identity?.traits).toEqual({ name: 'John' });
322
+ });
323
+
324
+ test('maps segment override priorities correctly', () => {
325
+ // When - using fixture which has segment with priority
326
+ const context = getEvaluationContext(testEnvironment);
327
+
328
+ // Then - verify regular_segment has a feature override
329
+ const segment = context.segments!['1'];
330
+ expect(segment.overrides?.length).toBeGreaterThan(0);
331
+
332
+ // The segment override from the fixture has no explicit priority, should be undefined
333
+ const segmentOverride = segment.overrides?.[0]!;
334
+ expect(segmentOverride.name).toBe('some_feature');
335
+ expect(segmentOverride.priority).toBeUndefined();
336
+ });
337
+
338
+ test('handles multiple identity overrides with same features', () => {
339
+ // Given - the fixture already has identity override with 'overridden-id'
340
+ // Verify it's mapped correctly
341
+ const context = getEvaluationContext(testEnvironment);
342
+
343
+ // Then
344
+ const overrideSegments = Object.values(context.segments!).filter(
345
+ s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
346
+ );
347
+
348
+ // The fixture has one identity override
349
+ expect(overrideSegments.length).toBe(1);
350
+ expect(overrideSegments[0].rules?.[0].conditions?.[0].value).toContain('overridden-id');
351
+ expect(overrideSegments[0].overrides?.length).toBe(1);
352
+ });
353
+ });