flagsmith-nodejs 1.1.3 → 2.0.0-beta.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 (65) hide show
  1. package/.github/workflows/pull_request.yaml +33 -0
  2. package/.gitmodules +3 -0
  3. package/.husky/pre-commit +6 -0
  4. package/.tool-versions +1 -0
  5. package/example/README.md +0 -6
  6. package/example/package-lock.json +1070 -2
  7. package/example/package.json +4 -3
  8. package/example/server/api/index.js +23 -18
  9. package/example/server/index.js +7 -6
  10. package/flagsmith-engine/environments/integrations/models.ts +4 -0
  11. package/flagsmith-engine/environments/models.ts +50 -0
  12. package/flagsmith-engine/environments/util.ts +29 -0
  13. package/flagsmith-engine/features/constants.ts +4 -0
  14. package/flagsmith-engine/features/models.ts +105 -0
  15. package/flagsmith-engine/features/util.ts +38 -0
  16. package/flagsmith-engine/identities/models.ts +60 -0
  17. package/flagsmith-engine/identities/traits/models.ts +9 -0
  18. package/flagsmith-engine/identities/util.ts +30 -0
  19. package/flagsmith-engine/index.ts +93 -0
  20. package/flagsmith-engine/organisations/models.ts +25 -0
  21. package/flagsmith-engine/organisations/util.ts +11 -0
  22. package/flagsmith-engine/projects/models.ts +22 -0
  23. package/flagsmith-engine/projects/util.ts +18 -0
  24. package/flagsmith-engine/segments/constants.ts +31 -0
  25. package/flagsmith-engine/segments/evaluators.ts +72 -0
  26. package/flagsmith-engine/segments/models.ts +103 -0
  27. package/flagsmith-engine/segments/util.ts +29 -0
  28. package/flagsmith-engine/utils/collections.ts +14 -0
  29. package/flagsmith-engine/utils/errors.ts +1 -0
  30. package/flagsmith-engine/utils/hashing/index.ts +52 -0
  31. package/flagsmith-engine/utils/index.ts +10 -0
  32. package/index.ts +6 -0
  33. package/jest.config.js +5 -0
  34. package/package.json +30 -12
  35. package/sdk/analytics.ts +60 -0
  36. package/sdk/errors.ts +2 -0
  37. package/sdk/index.ts +329 -0
  38. package/sdk/models.ts +145 -0
  39. package/sdk/polling_manager.ts +31 -0
  40. package/sdk/utils.ts +45 -0
  41. package/tests/engine/e2e/engine.test.ts +51 -0
  42. package/tests/engine/engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json +12393 -0
  43. package/tests/engine/engine-tests/engine-test-data/readme.md +30 -0
  44. package/tests/engine/unit/egine.test.ts +96 -0
  45. package/tests/engine/unit/environments/builder.test.ts +148 -0
  46. package/tests/engine/unit/environments/models.test.ts +49 -0
  47. package/tests/engine/unit/features/models.test.ts +72 -0
  48. package/tests/engine/unit/identities/identities_builders.test.ts +85 -0
  49. package/tests/engine/unit/identities/identities_models.test.ts +105 -0
  50. package/tests/engine/unit/organization/models.test.ts +12 -0
  51. package/tests/engine/unit/segments/segments_model.test.ts +101 -0
  52. package/tests/engine/unit/segments/util.ts +151 -0
  53. package/tests/engine/unit/utils.ts +114 -0
  54. package/tests/index.js +0 -0
  55. package/tests/sdk/analytics.test.ts +43 -0
  56. package/tests/sdk/data/environment.json +70 -0
  57. package/tests/sdk/data/flags.json +20 -0
  58. package/tests/sdk/data/identities.json +29 -0
  59. package/tests/sdk/flagsmith.test.ts +203 -0
  60. package/tests/sdk/polling.test.ts +34 -0
  61. package/tests/sdk/utils.ts +39 -0
  62. package/tsconfig.json +19 -0
  63. package/flagsmith-core.js +0 -243
  64. package/index.d.ts +0 -85
  65. package/index.js +0 -3
@@ -0,0 +1,101 @@
1
+ import {
2
+ ALL_RULE,
3
+ ANY_RULE,
4
+ CONDITION_OPERATORS,
5
+ NONE_RULE
6
+ } from '../../../../flagsmith-engine/segments/constants';
7
+ import {
8
+ all,
9
+ any,
10
+ SegmentConditionModel,
11
+ SegmentRuleModel
12
+ } from '../../../../flagsmith-engine/segments/models';
13
+
14
+ const conditionMatchCases: [string, string | number | boolean, string, boolean][] = [
15
+ [CONDITION_OPERATORS.EQUAL, 'bar', 'bar', true],
16
+ [CONDITION_OPERATORS.EQUAL, 'bar', 'baz', false],
17
+ [CONDITION_OPERATORS.EQUAL, 1, '1', true],
18
+ [CONDITION_OPERATORS.EQUAL, 1, '2', false],
19
+ [CONDITION_OPERATORS.EQUAL, true, 'true', true],
20
+ [CONDITION_OPERATORS.EQUAL, false, 'false', true],
21
+ [CONDITION_OPERATORS.EQUAL, false, 'true', false],
22
+ [CONDITION_OPERATORS.EQUAL, true, 'false', false],
23
+ [CONDITION_OPERATORS.EQUAL, 1.23, '1.23', true],
24
+ [CONDITION_OPERATORS.EQUAL, 1.23, '4.56', false],
25
+ [CONDITION_OPERATORS.GREATER_THAN, 2, '1', true],
26
+ [CONDITION_OPERATORS.GREATER_THAN, 1, '1', false],
27
+ [CONDITION_OPERATORS.GREATER_THAN, 0, '1', false],
28
+ [CONDITION_OPERATORS.GREATER_THAN, 2.1, '2.0', true],
29
+ [CONDITION_OPERATORS.GREATER_THAN, 2.1, '2.1', false],
30
+ [CONDITION_OPERATORS.GREATER_THAN, 2.0, '2.1', false],
31
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 2, '1', true],
32
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 1, '1', true],
33
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 0, '1', false],
34
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 2.1, '2.0', true],
35
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 2.1, '2.1', true],
36
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, 2.0, '2.1', false],
37
+ [CONDITION_OPERATORS.LESS_THAN, 1, '2', true],
38
+ [CONDITION_OPERATORS.LESS_THAN, 1, '1', false],
39
+ [CONDITION_OPERATORS.LESS_THAN, 1, '0', false],
40
+ [CONDITION_OPERATORS.LESS_THAN, 2.0, '2.1', true],
41
+ [CONDITION_OPERATORS.LESS_THAN, 2.1, '2.1', false],
42
+ [CONDITION_OPERATORS.LESS_THAN, 2.1, '2.0', false],
43
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 1, '2', true],
44
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 1, '1', true],
45
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 1, '0', false],
46
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 2.0, '2.1', true],
47
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 2.1, '2.1', true],
48
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, 2.1, '2.0', false],
49
+ [CONDITION_OPERATORS.NOT_EQUAL, 'bar', 'baz', true],
50
+ [CONDITION_OPERATORS.NOT_EQUAL, 'bar', 'bar', false],
51
+ [CONDITION_OPERATORS.NOT_EQUAL, 1, '2', true],
52
+ [CONDITION_OPERATORS.NOT_EQUAL, 1, '1', false],
53
+ [CONDITION_OPERATORS.NOT_EQUAL, true, 'false', true],
54
+ [CONDITION_OPERATORS.NOT_EQUAL, false, 'true', true],
55
+ [CONDITION_OPERATORS.NOT_EQUAL, false, 'false', false],
56
+ [CONDITION_OPERATORS.NOT_EQUAL, true, 'true', false],
57
+ [CONDITION_OPERATORS.CONTAINS, 'bar', 'b', true],
58
+ [CONDITION_OPERATORS.CONTAINS, 'bar', 'bar', true],
59
+ [CONDITION_OPERATORS.CONTAINS, 'bar', 'baz', false],
60
+ [CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'b', false],
61
+ [CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'bar', false],
62
+ [CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'baz', true],
63
+ [CONDITION_OPERATORS.REGEX, 'foo', '[a-z]+', true],
64
+ [CONDITION_OPERATORS.REGEX, 'FOO', '[a-z]+', false]
65
+ ];
66
+
67
+ test('test_segment_condition_matches_trait_value', () => {
68
+ for (const testCase of conditionMatchCases) {
69
+ expect(
70
+ new SegmentConditionModel(testCase[0], testCase[2], 'foo').matchesTraitValue(
71
+ testCase[1]
72
+ )
73
+ ).toBe(testCase[3]);
74
+ }
75
+ });
76
+
77
+ test('test_segment_rule_none', () => {
78
+ const testCases: [boolean[], boolean][] = [
79
+ [[], true],
80
+ [[false], true],
81
+ [[false, false], true],
82
+ [[false, true], false],
83
+ [[true, true], false]
84
+ ];
85
+
86
+ for (const testCase of testCases) {
87
+ expect(SegmentRuleModel.none(testCase[0])).toBe(testCase[1]);
88
+ }
89
+ });
90
+
91
+ test('test_segment_rule_matching_function', () => {
92
+ const testCases: [string, CallableFunction][] = [
93
+ [ALL_RULE, all],
94
+ [ANY_RULE, any],
95
+ [NONE_RULE, SegmentRuleModel.none]
96
+ ];
97
+
98
+ for (const testCase of testCases) {
99
+ expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]);
100
+ }
101
+ });
@@ -0,0 +1,151 @@
1
+ import { ALL_RULE, ANY_RULE, EQUAL } from '../../../../flagsmith-engine/segments/constants';
2
+ import { SegmentModel } from '../../../../flagsmith-engine/segments/models';
3
+ import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util';
4
+
5
+ export const traitKey1 = 'email';
6
+ export const traitValue1 = 'user@example.com';
7
+
8
+ export const traitKey2 = 'num_purchase';
9
+ export const traitValue2 = '12';
10
+
11
+ export const traitKey_3 = 'date_joined';
12
+ export const traitValue3 = '2021-01-01';
13
+
14
+ export const emptySegment = new SegmentModel(1, 'empty_segment');
15
+
16
+ export const segmentSingleCondition = buildSegmentModel({
17
+ id: 2,
18
+ name: 'segment_one_condition',
19
+ rules: [
20
+ {
21
+ type: ALL_RULE,
22
+ conditions: [
23
+ {
24
+ operator: EQUAL,
25
+ property_: traitKey1,
26
+ value: traitValue1
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ });
32
+
33
+ export const segmentMultipleConditionsAll = buildSegmentModel({
34
+ id: 3,
35
+ name: 'segment_multiple_conditions_all',
36
+ rules: [
37
+ {
38
+ type: ALL_RULE,
39
+ conditions: [
40
+ {
41
+ operator: EQUAL,
42
+ property_: traitKey1,
43
+ value: traitValue1
44
+ },
45
+ {
46
+ operator: EQUAL,
47
+ property_: traitKey2,
48
+ value: traitValue2
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ });
54
+
55
+ export const segmentMultipleConditionsAny = buildSegmentModel({
56
+ id: 4,
57
+ name: 'segment_multiple_conditions_any',
58
+ rules: [
59
+ {
60
+ type: ANY_RULE,
61
+ conditions: [
62
+ {
63
+ operator: EQUAL,
64
+ property_: traitKey1,
65
+ value: traitValue1
66
+ },
67
+ {
68
+ operator: EQUAL,
69
+ property_: traitKey2,
70
+ value: traitValue2
71
+ }
72
+ ]
73
+ }
74
+ ]
75
+ });
76
+
77
+ export const segmentNestedRules = buildSegmentModel({
78
+ id: 5,
79
+ name: 'segment_nested_rules_all',
80
+ rules: [
81
+ {
82
+ type: ALL_RULE,
83
+ rules: [
84
+ {
85
+ type: ALL_RULE,
86
+ conditions: [
87
+ {
88
+ operator: EQUAL,
89
+ property_: traitKey1,
90
+ value: traitValue1
91
+ },
92
+ {
93
+ operator: EQUAL,
94
+ property_: traitKey2,
95
+ value: traitValue2
96
+ }
97
+ ]
98
+ },
99
+ {
100
+ type: ALL_RULE,
101
+ conditions: [
102
+ {
103
+ operator: EQUAL,
104
+ property_: traitKey_3,
105
+ value: traitValue3
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ ]
112
+ });
113
+
114
+ export const segmentConditionsAndNestedRules = buildSegmentModel({
115
+ id: 6,
116
+ name: 'segment_multiple_conditions_all_and_nested_rules',
117
+ rules: [
118
+ {
119
+ type: ALL_RULE,
120
+ conditions: [
121
+ {
122
+ operator: EQUAL,
123
+ property_: traitKey1,
124
+ value: traitValue1
125
+ }
126
+ ],
127
+ rules: [
128
+ {
129
+ type: ALL_RULE,
130
+ conditions: [
131
+ {
132
+ operator: EQUAL,
133
+ property_: traitKey2,
134
+ value: traitValue2
135
+ }
136
+ ]
137
+ },
138
+ {
139
+ type: ALL_RULE,
140
+ conditions: [
141
+ {
142
+ operator: EQUAL,
143
+ property_: traitKey_3,
144
+ value: traitValue3
145
+ }
146
+ ]
147
+ }
148
+ ]
149
+ }
150
+ ]
151
+ });
@@ -0,0 +1,114 @@
1
+ import { EnvironmentModel } from '../../../flagsmith-engine/environments/models';
2
+ import { CONSTANTS } from '../../../flagsmith-engine/features/constants';
3
+ import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models';
4
+ import { IdentityModel } from '../../../flagsmith-engine/identities/models';
5
+ import { TraitModel } from '../../../flagsmith-engine/identities/traits/models';
6
+ import { OrganisationModel } from '../../../flagsmith-engine/organisations/models';
7
+ import { ProjectModel } from '../../../flagsmith-engine/projects/models';
8
+ import { ALL_RULE, EQUAL } from '../../../flagsmith-engine/segments/constants';
9
+ import {
10
+ SegmentConditionModel,
11
+ SegmentModel,
12
+ SegmentRuleModel
13
+ } from '../../../flagsmith-engine/segments/models';
14
+
15
+ export const segmentConditionProperty = 'foo';
16
+ export const segmentConditionStringValue = 'bar';
17
+
18
+ export function segmentCondition() {
19
+ return new SegmentConditionModel(EQUAL, segmentConditionStringValue, segmentConditionProperty);
20
+ }
21
+
22
+ export function traitMatchingSegment() {
23
+ return new TraitModel(segmentCondition().property_ as string, segmentCondition().value);
24
+ }
25
+
26
+ export function organisation() {
27
+ return new OrganisationModel(1, 'test Org', true, false, true);
28
+ }
29
+
30
+ export function segmentRule() {
31
+ const rule = new SegmentRuleModel(ALL_RULE);
32
+ rule.conditions = [segmentCondition()];
33
+ return rule;
34
+ }
35
+
36
+ export function segment() {
37
+ const segment = new SegmentModel(1, 'test name');
38
+ segment.rules = [segmentRule()];
39
+ return segment;
40
+ }
41
+
42
+ export function project() {
43
+ const project = new ProjectModel(1, 'test project', false, organisation());
44
+ project.segments = [segment()];
45
+ return project;
46
+ }
47
+
48
+ export function feature1() {
49
+ return new FeatureModel(1, 'feature_1', CONSTANTS.STANDARD);
50
+ }
51
+
52
+ export function feature2() {
53
+ return new FeatureModel(2, 'feature_2', CONSTANTS.STANDARD);
54
+ }
55
+
56
+ export function environment() {
57
+ const env = new EnvironmentModel(1, 'api-key', project());
58
+
59
+ env.featureStates = [
60
+ new FeatureStateModel(feature1(), true, 1),
61
+ new FeatureStateModel(feature2(), false, 2)
62
+ ];
63
+
64
+ return env;
65
+ }
66
+
67
+ export function identity() {
68
+ return new IdentityModel(Date.now().toString(), [], [], environment().apiKey, 'identity_1');
69
+ }
70
+
71
+ export function identityInSegment() {
72
+ const identity = new IdentityModel(
73
+ Date.now().toString(),
74
+ [],
75
+ [],
76
+ environment().apiKey,
77
+ 'identity_2'
78
+ );
79
+
80
+ identity.identityTraits = [traitMatchingSegment()];
81
+
82
+ return identity;
83
+ }
84
+
85
+ export function getEnvironmentFeatureStateForFeatureByName(
86
+ environment: EnvironmentModel,
87
+ feature_name: string
88
+ ): FeatureStateModel | undefined {
89
+ const features = environment.featureStates.filter(fs => fs.feature.name === feature_name);
90
+ return features.length > 0 ? features[0] : undefined;
91
+ }
92
+
93
+ export function getEnvironmentFeatureStateForFeature(
94
+ environment: EnvironmentModel,
95
+ feature: FeatureModel
96
+ ): FeatureStateModel | undefined {
97
+ const f = environment.featureStates.find(f => f.feature === feature);
98
+ return f;
99
+ }
100
+
101
+ export function segmentOverrideFs() {
102
+ const fs = new FeatureStateModel(feature1(), false, 4);
103
+ fs.setValue('segment_override');
104
+ return fs;
105
+ }
106
+
107
+ export function environmentWithSegmentOverride(): EnvironmentModel {
108
+ const env = environment();
109
+ const segm = segment();
110
+
111
+ segm.featureStates.push(segmentOverrideFs());
112
+ env.project.segments.push(segm);
113
+ return env;
114
+ }
package/tests/index.js ADDED
File without changes
@@ -0,0 +1,43 @@
1
+ jest.mock('node-fetch');
2
+ import fetch from 'node-fetch';
3
+ import { analyticsProcessor } from './utils';
4
+
5
+ afterEach(() => {
6
+ jest.clearAllMocks();
7
+ });
8
+
9
+ test('test_analytics_processor_track_feature_updates_analytics_data', () => {
10
+ const aP = analyticsProcessor();
11
+ aP.trackFeature(1);
12
+ expect(aP.analyticsData[1]).toBe(1);
13
+
14
+ aP.trackFeature(1);
15
+ expect(aP.analyticsData[1]).toBe(2);
16
+ });
17
+
18
+ test('test_analytics_processor_flush_clears_analytics_data', async () => {
19
+ const aP = analyticsProcessor();
20
+ aP.trackFeature(1);
21
+ await aP.flush();
22
+ expect(aP.analyticsData).toStrictEqual({});
23
+ });
24
+
25
+ test('test_analytics_processor_flush_post_request_data_match_ananlytics_data', async () => {
26
+ const aP = analyticsProcessor();
27
+ aP.trackFeature(1);
28
+ aP.trackFeature(2);
29
+ await aP.flush();
30
+ expect(fetch).toHaveBeenCalledTimes(1);
31
+ expect(fetch).toHaveBeenCalledWith('http://testUrlanalytics/flags/', {
32
+ body: '{"1":1,"2":1}',
33
+ headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'test-key' },
34
+ method: 'POST',
35
+ timeout: 3
36
+ });
37
+ });
38
+
39
+ test('test_analytics_processor_flush_early_exit_if_analytics_data_is_empty', async () => {
40
+ const aP = analyticsProcessor();
41
+ await aP.flush();
42
+ expect(fetch).not.toHaveBeenCalled();
43
+ });
@@ -0,0 +1,70 @@
1
+ {
2
+ "api_key": "B62qaMZNwfiqT76p38ggrQ",
3
+ "project": {
4
+ "name": "Test project",
5
+ "organisation": {
6
+ "feature_analytics": false,
7
+ "name": "Test Org",
8
+ "id": 1,
9
+ "persist_trait_data": true,
10
+ "stop_serving_flags": false
11
+ },
12
+ "id": 1,
13
+ "hide_disabled_flags": false,
14
+ "segments": [
15
+ {
16
+ "name": "regular_segment",
17
+ "feature_states": [
18
+ {
19
+ "feature_state_value": "segment_override",
20
+ "multivariate_feature_state_values": [],
21
+ "django_id": 81027,
22
+ "feature": {
23
+ "name": "some_feature",
24
+ "type": "STANDARD",
25
+ "id": 1
26
+ },
27
+ "enabled": false
28
+ }
29
+ ],
30
+ "id": 1,
31
+ "rules": [
32
+ {
33
+ "type": "ALL",
34
+ "conditions": [],
35
+ "rules": [
36
+ {
37
+ "type": "ANY",
38
+ "conditions": [
39
+ {
40
+ "value": "40",
41
+ "property_": "age",
42
+ "operator": "LESS_THAN"
43
+ }
44
+ ],
45
+ "rules": []
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ },
53
+ "segment_overrides": [],
54
+ "id": 1,
55
+ "feature_states": [
56
+ {
57
+ "multivariate_feature_state_values": [],
58
+ "feature_state_value": "some-value",
59
+ "id": 1,
60
+ "featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
61
+ "feature": {
62
+ "name": "some_feature",
63
+ "type": "STANDARD",
64
+ "id": 1
65
+ },
66
+ "segment_id": null,
67
+ "enabled": true
68
+ }
69
+ ]
70
+ }
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "feature": {
5
+ "id": 1,
6
+ "name": "some_feature",
7
+ "created_date": "2019-08-27T14:53:45.698555Z",
8
+ "initial_value": null,
9
+ "description": null,
10
+ "default_enabled": false,
11
+ "type": "STANDARD",
12
+ "project": 1
13
+ },
14
+ "feature_state_value": "some-value",
15
+ "enabled": true,
16
+ "environment": 1,
17
+ "identity": null,
18
+ "feature_segment": null
19
+ }
20
+ ]
@@ -0,0 +1,29 @@
1
+ {
2
+ "traits": [
3
+ {
4
+ "id": 1,
5
+ "trait_key": "some_trait",
6
+ "trait_value": "some_value"
7
+ }
8
+ ],
9
+ "flags": [
10
+ {
11
+ "id": 1,
12
+ "feature": {
13
+ "id": 1,
14
+ "name": "some_feature",
15
+ "created_date": "2019-08-27T14:53:45.698555Z",
16
+ "initial_value": null,
17
+ "description": null,
18
+ "default_enabled": false,
19
+ "type": "STANDARD",
20
+ "project": 1
21
+ },
22
+ "feature_state_value": "some-value",
23
+ "enabled": true,
24
+ "environment": 1,
25
+ "identity": null,
26
+ "feature_segment": null
27
+ }
28
+ ]
29
+ }