flagsmith-nodejs 1.1.0 → 2.0.0-beta.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.
- package/.github/workflows/pull_request.yaml +33 -0
- package/.gitmodules +3 -0
- package/.husky/pre-commit +6 -0
- package/{LICENCE.md → LICENCE} +4 -5
- package/README.md +1 -1
- package/build/flagsmith-engine/environments/integrations/models.d.ts +4 -0
- package/build/flagsmith-engine/environments/integrations/models.js +8 -0
- package/build/flagsmith-engine/environments/models.d.ts +25 -0
- package/build/flagsmith-engine/environments/models.js +40 -0
- package/build/flagsmith-engine/environments/util.d.ts +3 -0
- package/build/flagsmith-engine/environments/util.js +19 -0
- package/build/flagsmith-engine/features/constants.d.ts +4 -0
- package/build/flagsmith-engine/features/constants.js +7 -0
- package/build/flagsmith-engine/features/models.d.ts +32 -0
- package/build/flagsmith-engine/features/models.js +85 -0
- package/build/flagsmith-engine/features/util.d.ts +3 -0
- package/build/flagsmith-engine/features/util.js +20 -0
- package/build/flagsmith-engine/identities/models.d.ts +15 -0
- package/build/flagsmith-engine/identities/models.js +47 -0
- package/build/flagsmith-engine/identities/traits/models.d.ts +5 -0
- package/build/flagsmith-engine/identities/traits/models.js +12 -0
- package/build/flagsmith-engine/identities/util.d.ts +4 -0
- package/build/flagsmith-engine/identities/util.js +22 -0
- package/build/flagsmith-engine/index.d.ts +8 -0
- package/build/flagsmith-engine/index.js +61 -0
- package/build/flagsmith-engine/organisations/models.d.ts +9 -0
- package/build/flagsmith-engine/organisations/models.js +21 -0
- package/build/flagsmith-engine/organisations/util.d.ts +2 -0
- package/build/flagsmith-engine/organisations/util.js +8 -0
- package/build/flagsmith-engine/projects/models.d.ts +10 -0
- package/build/flagsmith-engine/projects/models.js +17 -0
- package/build/flagsmith-engine/projects/util.d.ts +2 -0
- package/build/flagsmith-engine/projects/util.js +15 -0
- package/build/flagsmith-engine/segments/constants.d.ts +26 -0
- package/build/flagsmith-engine/segments/constants.js +31 -0
- package/build/flagsmith-engine/segments/evaluators.d.ts +6 -0
- package/build/flagsmith-engine/segments/evaluators.js +29 -0
- package/build/flagsmith-engine/segments/models.d.ts +31 -0
- package/build/flagsmith-engine/segments/models.js +83 -0
- package/build/flagsmith-engine/segments/util.d.ts +4 -0
- package/build/flagsmith-engine/segments/util.js +23 -0
- package/build/flagsmith-engine/utils/collections.d.ts +4 -0
- package/build/flagsmith-engine/utils/collections.js +16 -0
- package/build/flagsmith-engine/utils/errors.d.ts +2 -0
- package/build/flagsmith-engine/utils/errors.js +6 -0
- package/build/flagsmith-engine/utils/hashing/index.d.ts +9 -0
- package/build/flagsmith-engine/utils/hashing/index.js +54 -0
- package/build/flagsmith-engine/utils/index.d.ts +1 -0
- package/build/flagsmith-engine/utils/index.js +14 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +11 -0
- package/build/sdk/analytics.d.ts +28 -0
- package/build/sdk/analytics.js +60 -0
- package/build/sdk/errors.d.ts +4 -0
- package/build/sdk/errors.js +9 -0
- package/build/sdk/index.d.ts +98 -0
- package/build/sdk/index.js +215 -0
- package/build/sdk/models.d.ts +55 -0
- package/build/sdk/models.js +102 -0
- package/build/sdk/polling_manager.d.ts +9 -0
- package/build/sdk/polling_manager.js +31 -0
- package/build/sdk/utils.d.ts +12 -0
- package/build/sdk/utils.js +45 -0
- package/example/README.md +0 -6
- package/example/package-lock.json +1070 -2
- package/example/package.json +4 -3
- package/example/server/api/index.js +18 -18
- package/example/server/index.js +4 -6
- package/flagsmith-engine/environments/integrations/models.ts +4 -0
- package/flagsmith-engine/environments/models.ts +50 -0
- package/flagsmith-engine/environments/util.ts +29 -0
- package/flagsmith-engine/features/constants.ts +4 -0
- package/flagsmith-engine/features/models.ts +105 -0
- package/flagsmith-engine/features/util.ts +38 -0
- package/flagsmith-engine/identities/models.ts +60 -0
- package/flagsmith-engine/identities/traits/models.ts +9 -0
- package/flagsmith-engine/identities/util.ts +30 -0
- package/flagsmith-engine/index.ts +93 -0
- package/flagsmith-engine/organisations/models.ts +25 -0
- package/flagsmith-engine/organisations/util.ts +11 -0
- package/flagsmith-engine/projects/models.ts +22 -0
- package/flagsmith-engine/projects/util.ts +18 -0
- package/flagsmith-engine/segments/constants.ts +31 -0
- package/flagsmith-engine/segments/evaluators.ts +72 -0
- package/flagsmith-engine/segments/models.ts +103 -0
- package/flagsmith-engine/segments/util.ts +29 -0
- package/flagsmith-engine/utils/collections.ts +14 -0
- package/flagsmith-engine/utils/errors.ts +1 -0
- package/flagsmith-engine/utils/hashing/index.ts +52 -0
- package/flagsmith-engine/utils/index.ts +10 -0
- package/index.ts +6 -0
- package/jest.config.js +5 -0
- package/package.json +28 -12
- package/sdk/analytics.ts +60 -0
- package/sdk/errors.ts +2 -0
- package/sdk/index.ts +270 -0
- package/sdk/models.ts +145 -0
- package/sdk/polling_manager.ts +31 -0
- package/sdk/utils.ts +45 -0
- package/tests/engine/e2e/engine.test.ts +51 -0
- package/tests/engine/engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json +12393 -0
- package/tests/engine/engine-tests/engine-test-data/readme.md +30 -0
- package/tests/engine/unit/egine.test.ts +96 -0
- package/tests/engine/unit/environments/builder.test.ts +148 -0
- package/tests/engine/unit/environments/models.test.ts +49 -0
- package/tests/engine/unit/features/models.test.ts +72 -0
- package/tests/engine/unit/identities/identities_builders.test.ts +85 -0
- package/tests/engine/unit/identities/identities_models.test.ts +105 -0
- package/tests/engine/unit/organization/models.test.ts +12 -0
- package/tests/engine/unit/segments/segments_model.test.ts +101 -0
- package/tests/engine/unit/segments/util.ts +151 -0
- package/tests/engine/unit/utils.ts +114 -0
- package/tests/index.js +0 -0
- package/tests/sdk/analytics.test.ts +43 -0
- package/tests/sdk/data/environment.json +33 -0
- package/tests/sdk/data/flags.json +20 -0
- package/tests/sdk/data/identities.json +29 -0
- package/tests/sdk/flagsmith.test.ts +184 -0
- package/tests/sdk/polling.test.ts +34 -0
- package/tests/sdk/utils.ts +39 -0
- package/tsconfig.json +19 -0
- package/.idea/codeStyles/Project.xml +0 -52
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/flagsmith-core.js +0 -241
- package/index.d.ts +0 -85
- package/index.js +0 -3
package/example/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "flagsmith-nodejs-example",
|
|
3
3
|
"version": "0.1.1",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "An example using the flagsmith nodejs library",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"start": "node --inspect ./server",
|
|
7
|
+
"start": "cd ../ && npm run build && cd ./example && node --inspect ./server",
|
|
8
8
|
"dev": "nodemon --exec npm start",
|
|
9
9
|
"test": "nodemon ./server/tests/"
|
|
10
10
|
},
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"npm": "3.10.x"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"flagsmith-nodejs": "^2.0.0-beta.1",
|
|
18
19
|
"node-cache": "^5.1.2",
|
|
19
20
|
"ssg-node-express": "4.16.4",
|
|
20
21
|
"ssg-util": "0.0.3"
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
const Router = require('express').Router;
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
const Flagsmith = require('../../../build');
|
|
3
|
+
const environmentKey = '';
|
|
4
|
+
if (!environmentKey) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
'Please generate a Server Side SDK Key in environment settings to run the example'
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
const flagsmith = new Flagsmith({
|
|
10
|
+
environmentKey,
|
|
11
|
+
enableLocalEvaluation: true,
|
|
12
|
+
defaultFlagHandler: str => {
|
|
13
|
+
return { enabled: false, isDefault: true, value: null };
|
|
14
|
+
}
|
|
12
15
|
});
|
|
13
16
|
|
|
14
17
|
module.exports = () => {
|
|
15
18
|
const api = Router();
|
|
16
19
|
|
|
17
20
|
api.get('/', async (req, res) => {
|
|
18
|
-
const
|
|
19
|
-
res.json({ font_size });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
api.get('/flags', async (req, res) => {
|
|
23
|
-
const flags = await flagsmith.getFlags();
|
|
21
|
+
const flags = await flagsmith.getEnvironmentFlags();
|
|
24
22
|
res.json(flags);
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
api.get('/:user', async (req, res) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
26
|
+
const flags = await flagsmith.getIdentityFlags(req.params.user, { checkout_v2: 1 });
|
|
27
|
+
const fontSize = flags.getFeatureValue('font_size');
|
|
28
|
+
const checkoutV2 = flags.isFeatureEnabled('checkout_v2');
|
|
29
|
+
res.json({ fontSize, checkoutV2 });
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
return api;
|
package/example/server/index.js
CHANGED
|
@@ -12,17 +12,15 @@ app.server = http.createServer(app);
|
|
|
12
12
|
app.use(bodyParser.json());
|
|
13
13
|
|
|
14
14
|
// api router
|
|
15
|
-
app.use('/', api());
|
|
15
|
+
app.use('/api', api());
|
|
16
16
|
|
|
17
17
|
app.server.listen(PORT);
|
|
18
18
|
console.log('Server started on port ' + app.server.address().port);
|
|
19
19
|
console.log();
|
|
20
|
-
console.log('Go to http://localhost:' + PORT + '/');
|
|
21
|
-
console.log('To get an example
|
|
20
|
+
console.log('Go to http://localhost:' + PORT + '/api');
|
|
21
|
+
console.log('To get an example response for getFlags');
|
|
22
22
|
console.log();
|
|
23
|
-
console.log('Go to http://localhost:' + PORT + '/flagsmith_sample_user');
|
|
23
|
+
console.log('Go to http://localhost:' + PORT + '/api/flagsmith_sample_user');
|
|
24
24
|
console.log('To get an example feature state for a user');
|
|
25
|
-
console.log('Go to http://localhost:' + PORT + '/flags');
|
|
26
|
-
console.log('To get an example response for getFlags');
|
|
27
25
|
|
|
28
26
|
module.exports = app;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FeatureStateModel } from '../features/models';
|
|
2
|
+
import { ProjectModel } from '../projects/models';
|
|
3
|
+
import { IntegrationModel } from './integrations/models';
|
|
4
|
+
|
|
5
|
+
export class EnvironmentAPIKeyModel {
|
|
6
|
+
id: number;
|
|
7
|
+
key: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
name: string;
|
|
10
|
+
clientApiKey: string;
|
|
11
|
+
expiresAt?: number;
|
|
12
|
+
active = true;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
id: number,
|
|
16
|
+
key: string,
|
|
17
|
+
createdAt: number,
|
|
18
|
+
name: string,
|
|
19
|
+
clientApiKey: string,
|
|
20
|
+
expiresAt?: number
|
|
21
|
+
) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.key = key;
|
|
24
|
+
this.createdAt = createdAt;
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.clientApiKey = clientApiKey;
|
|
27
|
+
this.expiresAt = expiresAt;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
isValid() {
|
|
31
|
+
return !!this.active && (!this.expiresAt || this.expiresAt > Date.now());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class EnvironmentModel {
|
|
36
|
+
id: number;
|
|
37
|
+
apiKey: string;
|
|
38
|
+
project: ProjectModel;
|
|
39
|
+
featureStates: FeatureStateModel[] = [];
|
|
40
|
+
amplitude_config?: IntegrationModel;
|
|
41
|
+
segment_config?: IntegrationModel;
|
|
42
|
+
mixpanel_config?: IntegrationModel;
|
|
43
|
+
heap_config?: IntegrationModel;
|
|
44
|
+
|
|
45
|
+
constructor(id: number, apiKey: string, project: ProjectModel) {
|
|
46
|
+
this.id = id;
|
|
47
|
+
this.apiKey = apiKey;
|
|
48
|
+
this.project = project;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildFeatureStateModel } from '../features/util';
|
|
2
|
+
import { buildProjectModel } from '../projects/util';
|
|
3
|
+
import { EnvironmentAPIKeyModel, EnvironmentModel } from './models';
|
|
4
|
+
|
|
5
|
+
export function buildEnvironmentModel(environmentJSON: any) {
|
|
6
|
+
const project = buildProjectModel(environmentJSON.project);
|
|
7
|
+
const featureStates = environmentJSON.feature_states.map((fs: any) =>
|
|
8
|
+
buildFeatureStateModel(fs)
|
|
9
|
+
);
|
|
10
|
+
const environmentModel = new EnvironmentModel(
|
|
11
|
+
environmentJSON.id,
|
|
12
|
+
environmentJSON.api_key,
|
|
13
|
+
project
|
|
14
|
+
);
|
|
15
|
+
environmentModel.featureStates = featureStates;
|
|
16
|
+
return environmentModel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildEnvironmentAPIKeyModel(apiKeyJSON: any): EnvironmentAPIKeyModel {
|
|
20
|
+
const model = new EnvironmentAPIKeyModel(
|
|
21
|
+
apiKeyJSON.id,
|
|
22
|
+
apiKeyJSON.key,
|
|
23
|
+
Date.parse(apiKeyJSON.created_at),
|
|
24
|
+
apiKeyJSON.name,
|
|
25
|
+
apiKeyJSON.client_api_key
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return model;
|
|
29
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { getHashedPercentateForObjIds } from '../utils/hashing';
|
|
3
|
+
|
|
4
|
+
export class FeatureModel {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
|
|
9
|
+
constructor(id: number, name: string, type: string) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.type = type;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
eq(other: FeatureModel) {
|
|
16
|
+
return !!other && this.id === other.id;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class MultivariateFeatureOptionModel {
|
|
21
|
+
value: any;
|
|
22
|
+
id: number | undefined;
|
|
23
|
+
|
|
24
|
+
constructor(value: any, id?: number) {
|
|
25
|
+
this.value = value;
|
|
26
|
+
this.id = id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class MultivariateFeatureStateValueModel {
|
|
31
|
+
multivariateFeatureOption: MultivariateFeatureOptionModel;
|
|
32
|
+
percentageAllocation: number;
|
|
33
|
+
id: number;
|
|
34
|
+
mvFsValueUuid: string = uuidv4();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
multivariate_feature_option: MultivariateFeatureOptionModel,
|
|
38
|
+
percentage_allocation: number,
|
|
39
|
+
id: number,
|
|
40
|
+
mvFsValueUuid?: string
|
|
41
|
+
) {
|
|
42
|
+
this.id = id;
|
|
43
|
+
this.percentageAllocation = percentage_allocation;
|
|
44
|
+
this.multivariateFeatureOption = multivariate_feature_option;
|
|
45
|
+
this.mvFsValueUuid = mvFsValueUuid || this.mvFsValueUuid;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class FeatureStateModel {
|
|
50
|
+
feature: FeatureModel;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
djangoID: number;
|
|
53
|
+
featurestateUUID: string = uuidv4();
|
|
54
|
+
private value: any;
|
|
55
|
+
multivariateFeatureStateValues: MultivariateFeatureStateValueModel[] = [];
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
feature: FeatureModel,
|
|
59
|
+
enabled: boolean,
|
|
60
|
+
djangoID: number,
|
|
61
|
+
value?: any,
|
|
62
|
+
featurestateUuid: string = uuidv4()
|
|
63
|
+
) {
|
|
64
|
+
this.feature = feature;
|
|
65
|
+
this.enabled = enabled;
|
|
66
|
+
this.djangoID = djangoID;
|
|
67
|
+
this.value = value;
|
|
68
|
+
this.featurestateUUID = featurestateUuid;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setValue(value: any) {
|
|
72
|
+
this.value = value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getValue(identityId?: number | string) {
|
|
76
|
+
if (!!identityId && this.multivariateFeatureStateValues.length > 0) {
|
|
77
|
+
return this.getMultivariateValue(identityId);
|
|
78
|
+
}
|
|
79
|
+
return this.value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get_feature_state_value() {
|
|
83
|
+
return this.getValue();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getMultivariateValue(identityID: number | string) {
|
|
87
|
+
const percentageValue = getHashedPercentateForObjIds([
|
|
88
|
+
this.djangoID || this.featurestateUUID,
|
|
89
|
+
identityID
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
let startPercentage = 0;
|
|
93
|
+
const sortedF = this.multivariateFeatureStateValues.sort((a, b) =>
|
|
94
|
+
!!(a.id && b.id) ? a.id - b.id : a.mvFsValueUuid > b.mvFsValueUuid ? -1 : 1
|
|
95
|
+
);
|
|
96
|
+
for (const myValue of sortedF) {
|
|
97
|
+
const limit = myValue.percentageAllocation + startPercentage;
|
|
98
|
+
if (startPercentage <= percentageValue && percentageValue < limit) {
|
|
99
|
+
return myValue.multivariateFeatureOption.value;
|
|
100
|
+
}
|
|
101
|
+
startPercentage = limit;
|
|
102
|
+
}
|
|
103
|
+
return this.value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureModel,
|
|
3
|
+
FeatureStateModel,
|
|
4
|
+
MultivariateFeatureOptionModel,
|
|
5
|
+
MultivariateFeatureStateValueModel
|
|
6
|
+
} from './models';
|
|
7
|
+
|
|
8
|
+
export function buildFeatureModel(featuresModelJSON: any): FeatureModel {
|
|
9
|
+
return new FeatureModel(featuresModelJSON.id, featuresModelJSON.name, featuresModelJSON.type);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStateModel {
|
|
13
|
+
const featureStateModel = new FeatureStateModel(
|
|
14
|
+
buildFeatureModel(featuresStateModelJSON.feature),
|
|
15
|
+
featuresStateModelJSON.enabled,
|
|
16
|
+
featuresStateModelJSON.django_id,
|
|
17
|
+
featuresStateModelJSON.feature_state_value,
|
|
18
|
+
featuresStateModelJSON.uuid
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const multivariateFeatureStateValues = featuresStateModelJSON.multivariate_feature_state_values
|
|
22
|
+
? featuresStateModelJSON.multivariate_feature_state_values.map((fsv: any) => {
|
|
23
|
+
const featureOption = new MultivariateFeatureOptionModel(
|
|
24
|
+
fsv.multivariate_feature_option.value,
|
|
25
|
+
fsv.multivariate_feature_option.id
|
|
26
|
+
);
|
|
27
|
+
return new MultivariateFeatureStateValueModel(
|
|
28
|
+
featureOption,
|
|
29
|
+
fsv.percentage_allocation,
|
|
30
|
+
fsv.id
|
|
31
|
+
);
|
|
32
|
+
})
|
|
33
|
+
: [];
|
|
34
|
+
|
|
35
|
+
featureStateModel.multivariateFeatureStateValues = multivariateFeatureStateValues;
|
|
36
|
+
|
|
37
|
+
return featureStateModel;
|
|
38
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { FeatureStateModel } from '../features/models';
|
|
2
|
+
import { IdentityFeaturesList } from '../utils/collections';
|
|
3
|
+
import { TraitModel } from './traits/models';
|
|
4
|
+
|
|
5
|
+
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
|
|
7
|
+
export class IdentityModel {
|
|
8
|
+
identifier: string;
|
|
9
|
+
environmentApiKey: string;
|
|
10
|
+
createdDate?: number;
|
|
11
|
+
identityFeatures: IdentityFeaturesList;
|
|
12
|
+
identityTraits: TraitModel[];
|
|
13
|
+
identityUuid: string;
|
|
14
|
+
djangoID: number | undefined;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
created_date: string,
|
|
18
|
+
identityTraits: TraitModel[],
|
|
19
|
+
identityFeatures: IdentityFeaturesList,
|
|
20
|
+
environmentApiKey: string,
|
|
21
|
+
identifier: string,
|
|
22
|
+
identityUuid?: string
|
|
23
|
+
) {
|
|
24
|
+
this.identityUuid = identityUuid || uuidv4();
|
|
25
|
+
this.createdDate = Date.parse(created_date) || Date.now();
|
|
26
|
+
this.identityTraits = identityTraits;
|
|
27
|
+
this.identityFeatures = new IdentityFeaturesList(...identityFeatures);
|
|
28
|
+
this.environmentApiKey = environmentApiKey;
|
|
29
|
+
this.identifier = identifier;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get compositeKey() {
|
|
33
|
+
return IdentityModel.generateCompositeKey(this.environmentApiKey, this.identifier);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static generateCompositeKey(env_key: string, identifier: string) {
|
|
37
|
+
return `${env_key}_${identifier}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update_traits(traits: TraitModel[]) {
|
|
41
|
+
const existingTraits: Map<string, TraitModel> = new Map();
|
|
42
|
+
for (const trait of this.identityTraits) {
|
|
43
|
+
existingTraits.set(trait.traitKey, trait);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const trait of traits) {
|
|
47
|
+
if (!!trait.traitValue) {
|
|
48
|
+
existingTraits.set(trait.traitKey, trait);
|
|
49
|
+
} else {
|
|
50
|
+
existingTraits.delete(trait.traitKey);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.identityTraits = [];
|
|
55
|
+
|
|
56
|
+
for (const [k, v] of existingTraits.entries()) {
|
|
57
|
+
this.identityTraits.push(v);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildFeatureStateModel } from '../features/util';
|
|
2
|
+
import { IdentityFeaturesList } from '../utils/collections';
|
|
3
|
+
import { IdentityModel } from './models';
|
|
4
|
+
import { TraitModel } from './traits/models';
|
|
5
|
+
|
|
6
|
+
export function buildTraitModel(traitJSON: any): TraitModel {
|
|
7
|
+
return new TraitModel(traitJSON.trait_key, traitJSON.trait_value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildIdentityModel(identityJSON: any): IdentityModel {
|
|
11
|
+
const featureList = identityJSON.identity_features
|
|
12
|
+
? new IdentityFeaturesList(
|
|
13
|
+
...identityJSON.identity_features.map((f: any) => buildFeatureStateModel(f))
|
|
14
|
+
)
|
|
15
|
+
: [];
|
|
16
|
+
|
|
17
|
+
const model = new IdentityModel(
|
|
18
|
+
identityJSON.created_date,
|
|
19
|
+
identityJSON.identity_traits
|
|
20
|
+
? identityJSON.identity_traits.map((trait: any) => buildTraitModel(trait))
|
|
21
|
+
: [],
|
|
22
|
+
featureList,
|
|
23
|
+
identityJSON.environment_api_key,
|
|
24
|
+
identityJSON.identifier,
|
|
25
|
+
identityJSON.identity_uuid
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
model.djangoID = identityJSON.django_id;
|
|
29
|
+
return model;
|
|
30
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { EnvironmentModel } from './environments/models';
|
|
2
|
+
import { FeatureStateModel } from './features/models';
|
|
3
|
+
import { IdentityModel } from './identities/models';
|
|
4
|
+
import { TraitModel } from './identities/traits/models';
|
|
5
|
+
import { getIdentitySegments } from './segments/evaluators';
|
|
6
|
+
import { SegmentModel } from './segments/models';
|
|
7
|
+
import { FeatureStateNotFound } from './utils/errors';
|
|
8
|
+
|
|
9
|
+
function getIdentityFeatureStatesDict(
|
|
10
|
+
environment: EnvironmentModel,
|
|
11
|
+
identity: IdentityModel,
|
|
12
|
+
overrideTraits?: TraitModel[]
|
|
13
|
+
) {
|
|
14
|
+
// Get feature states from the environment
|
|
15
|
+
const featureStates: { [key: number]: FeatureStateModel } = {};
|
|
16
|
+
for (const fs of environment.featureStates) {
|
|
17
|
+
featureStates[fs.feature.id] = fs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Override with any feature states defined by matching segments
|
|
21
|
+
const identitySegments: SegmentModel[] = getIdentitySegments(
|
|
22
|
+
environment,
|
|
23
|
+
identity,
|
|
24
|
+
overrideTraits
|
|
25
|
+
);
|
|
26
|
+
for (const matchingSegment of identitySegments) {
|
|
27
|
+
for (const featureState of matchingSegment.featureStates) {
|
|
28
|
+
// note that feature states are stored on the segment in descending priority
|
|
29
|
+
// order so we only care that the last one is added
|
|
30
|
+
// TODO: can we optimise this?
|
|
31
|
+
featureStates[featureState.feature.id] = featureState;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Override with any feature states defined directly the identity
|
|
36
|
+
for (const fs of identity.identityFeatures || []) {
|
|
37
|
+
if (featureStates[fs.feature.id]) {
|
|
38
|
+
featureStates[fs.feature.id] = fs;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return featureStates;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getIdentityFeatureState(
|
|
45
|
+
environment: EnvironmentModel,
|
|
46
|
+
identity: IdentityModel,
|
|
47
|
+
featureName: string,
|
|
48
|
+
overrideTraits?: TraitModel[]
|
|
49
|
+
): FeatureStateModel {
|
|
50
|
+
const featureStates = getIdentityFeatureStatesDict(environment, identity, overrideTraits);
|
|
51
|
+
|
|
52
|
+
const matchingFeature = Object.values(featureStates).filter(
|
|
53
|
+
f => f.feature.name === featureName
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (matchingFeature.length === 0) {
|
|
57
|
+
throw new FeatureStateNotFound('Feature State Not Found');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return matchingFeature[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getIdentityFeatureStates(
|
|
64
|
+
environment: EnvironmentModel,
|
|
65
|
+
identity: IdentityModel,
|
|
66
|
+
overrideTraits?: TraitModel[]
|
|
67
|
+
): FeatureStateModel[] {
|
|
68
|
+
const featureStates = Object.values(
|
|
69
|
+
getIdentityFeatureStatesDict(environment, identity, overrideTraits)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (environment.project.hideDisabledFlags) {
|
|
73
|
+
return featureStates.filter(fs => !!fs.enabled);
|
|
74
|
+
}
|
|
75
|
+
return featureStates;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getEnvironmentFeatureState(environment: EnvironmentModel, featureName: string) {
|
|
79
|
+
const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName);
|
|
80
|
+
|
|
81
|
+
if (featuresStates.length === 0) {
|
|
82
|
+
throw new Error('Feature State Not Found');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return featuresStates[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getEnvironmentFeatureStates(environment: EnvironmentModel): FeatureStateModel[] {
|
|
89
|
+
if (environment.project.hideDisabledFlags) {
|
|
90
|
+
return environment.featureStates.filter(fs => !!fs.enabled);
|
|
91
|
+
}
|
|
92
|
+
return environment.featureStates;
|
|
93
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class OrganisationModel {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
featureAnalytics: boolean;
|
|
5
|
+
stopServingFlags: boolean;
|
|
6
|
+
persistTraitData: boolean;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
id: number,
|
|
10
|
+
name: string,
|
|
11
|
+
featureAnalytics: boolean,
|
|
12
|
+
stopServingFlags: boolean,
|
|
13
|
+
persistTraitData: boolean
|
|
14
|
+
) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.featureAnalytics = featureAnalytics;
|
|
18
|
+
this.stopServingFlags = stopServingFlags;
|
|
19
|
+
this.persistTraitData = persistTraitData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get unique_slug() {
|
|
23
|
+
return this.id.toString() + '-' + this.name;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OrganisationModel } from './models';
|
|
2
|
+
|
|
3
|
+
export function buildOrganizationModel(organizationJSON: any): OrganisationModel {
|
|
4
|
+
return new OrganisationModel(
|
|
5
|
+
organizationJSON.id,
|
|
6
|
+
organizationJSON.name,
|
|
7
|
+
organizationJSON.feature_analytics,
|
|
8
|
+
organizationJSON.stop_serving_flags,
|
|
9
|
+
organizationJSON.persist_trait_data
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OrganisationModel } from '../organisations/models';
|
|
2
|
+
import { SegmentModel } from '../segments/models';
|
|
3
|
+
|
|
4
|
+
export class ProjectModel {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
organisation: OrganisationModel;
|
|
8
|
+
hideDisabledFlags: boolean;
|
|
9
|
+
segments: SegmentModel[] = [];
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
id: number,
|
|
13
|
+
name: string,
|
|
14
|
+
hideDisabledFlags: boolean,
|
|
15
|
+
organization: OrganisationModel
|
|
16
|
+
) {
|
|
17
|
+
this.id = id;
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.hideDisabledFlags = hideDisabledFlags;
|
|
20
|
+
this.organisation = organization;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildOrganizationModel } from '../organisations/util';
|
|
2
|
+
import { SegmentModel } from '../segments/models';
|
|
3
|
+
import { buildSegmentModel } from '../segments/util';
|
|
4
|
+
import { ProjectModel } from './models';
|
|
5
|
+
|
|
6
|
+
export function buildProjectModel(projectJSON: any): ProjectModel {
|
|
7
|
+
const segments: SegmentModel[] = projectJSON['segments']
|
|
8
|
+
? projectJSON['segments'].map((s: any) => buildSegmentModel(s))
|
|
9
|
+
: [];
|
|
10
|
+
const model = new ProjectModel(
|
|
11
|
+
projectJSON.id,
|
|
12
|
+
projectJSON.name,
|
|
13
|
+
projectJSON.hide_disabled_flags,
|
|
14
|
+
buildOrganizationModel(projectJSON.organisation)
|
|
15
|
+
);
|
|
16
|
+
model.segments = segments;
|
|
17
|
+
return model;
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Segment Rules
|
|
2
|
+
export const ALL_RULE = 'ALL';
|
|
3
|
+
export const ANY_RULE = 'ANY';
|
|
4
|
+
export const NONE_RULE = 'NONE';
|
|
5
|
+
|
|
6
|
+
export const RULE_TYPES = [ALL_RULE, ANY_RULE, NONE_RULE];
|
|
7
|
+
|
|
8
|
+
// Segment Condition Operators
|
|
9
|
+
export const EQUAL = 'EQUAL';
|
|
10
|
+
export const GREATER_THAN = 'GREATER_THAN';
|
|
11
|
+
export const LESS_THAN = 'LESS_THAN';
|
|
12
|
+
export const LESS_THAN_INCLUSIVE = 'LESS_THAN_INCLUSIVE';
|
|
13
|
+
export const CONTAINS = 'CONTAINS';
|
|
14
|
+
export const GREATER_THAN_INCLUSIVE = 'GREATER_THAN_INCLUSIVE';
|
|
15
|
+
export const NOT_CONTAINS = 'NOT_CONTAINS';
|
|
16
|
+
export const NOT_EQUAL = 'NOT_EQUAL';
|
|
17
|
+
export const REGEX = 'REGEX';
|
|
18
|
+
export const PERCENTAGE_SPLIT = 'PERCENTAGE_SPLIT';
|
|
19
|
+
|
|
20
|
+
export const CONDITION_OPERATORS = {
|
|
21
|
+
EQUAL,
|
|
22
|
+
GREATER_THAN,
|
|
23
|
+
LESS_THAN,
|
|
24
|
+
LESS_THAN_INCLUSIVE,
|
|
25
|
+
CONTAINS,
|
|
26
|
+
GREATER_THAN_INCLUSIVE,
|
|
27
|
+
NOT_CONTAINS,
|
|
28
|
+
NOT_EQUAL,
|
|
29
|
+
REGEX,
|
|
30
|
+
PERCENTAGE_SPLIT
|
|
31
|
+
};
|