flagsmith-nodejs 1.0.9 → 2.0.0-beta.1
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/.idea/flagsmith-nodejs-client.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierignore +1 -0
- package/.prettierrc.js +9 -0
- package/CONTRIBUTING.md +5 -4
- package/{LICENCE.md → LICENCE} +5 -6
- 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 +60 -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 +18 -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/hashing/index.d.ts +1 -0
- package/build/flagsmith-engine/utils/hashing/index.js +56 -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 +2 -0
- package/build/index.js +7 -0
- package/build/sdk/analytics.d.ts +28 -0
- package/build/sdk/analytics.js +81 -0
- package/build/sdk/errors.d.ts +4 -0
- package/build/sdk/errors.js +9 -0
- package/build/sdk/index.d.ts +99 -0
- package/build/sdk/index.js +221 -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 +8 -14
- package/example/package-lock.json +12 -12
- package/example/package.json +4 -4
- package/example/server/api/index.js +19 -22
- package/example/server/index.js +4 -9
- 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 +92 -0
- package/flagsmith-engine/organisations/models.ts +25 -0
- package/flagsmith-engine/organisations/util.ts +11 -0
- package/flagsmith-engine/projects/models.ts +23 -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/hashing/index.ts +57 -0
- package/flagsmith-engine/utils/index.ts +10 -0
- package/index.ts +3 -0
- package/jest.config.js +5 -0
- package/package.json +24 -3
- package/sdk/analytics.ts +88 -0
- package/sdk/errors.ts +2 -0
- package/sdk/index.ts +282 -0
- package/sdk/models.ts +143 -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/flagsmith-core.js +0 -238
- package/index.d.ts +0 -78
- package/index.js +0 -5
|
@@ -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
|
+
created_at: number,
|
|
18
|
+
name: string,
|
|
19
|
+
client_api_key: string,
|
|
20
|
+
expires_at?: number
|
|
21
|
+
) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.key = key;
|
|
24
|
+
this.createdAt = created_at;
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.clientApiKey = client_api_key;
|
|
27
|
+
this.expiresAt = expires_at;
|
|
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, api_key: string, project: ProjectModel) {
|
|
46
|
+
this.id = id;
|
|
47
|
+
this.apiKey = api_key;
|
|
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
|
+
_value: any;
|
|
55
|
+
multivariateFeatureStateValues: MultivariateFeatureStateValueModel[] = [];
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
feature: FeatureModel,
|
|
59
|
+
enabled: boolean,
|
|
60
|
+
djangoID: number,
|
|
61
|
+
value?: any,
|
|
62
|
+
featurestate_uuid: string = uuidv4()
|
|
63
|
+
) {
|
|
64
|
+
this.feature = feature;
|
|
65
|
+
this.enabled = enabled;
|
|
66
|
+
this.djangoID = djangoID;
|
|
67
|
+
this._value = value;
|
|
68
|
+
this.featurestateUUID = featurestate_uuid;
|
|
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
|
+
identity_traits: TraitModel[],
|
|
19
|
+
identity_features: IdentityFeaturesList,
|
|
20
|
+
environment_api_key: string,
|
|
21
|
+
identifier: string,
|
|
22
|
+
identity_uuid?: string
|
|
23
|
+
) {
|
|
24
|
+
this.identityUuid = identity_uuid || uuidv4();
|
|
25
|
+
this.createdDate = Date.parse(created_date) || Date.now();
|
|
26
|
+
this.identityTraits = identity_traits;
|
|
27
|
+
this.identityFeatures = new IdentityFeaturesList(...identity_features);
|
|
28
|
+
this.environmentApiKey = environment_api_key;
|
|
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,92 @@
|
|
|
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
|
+
|
|
8
|
+
function getIdentityFeatureStatesDict(
|
|
9
|
+
environment: EnvironmentModel,
|
|
10
|
+
identity: IdentityModel,
|
|
11
|
+
override_traits?: TraitModel[]
|
|
12
|
+
) {
|
|
13
|
+
// Get feature states from the environment
|
|
14
|
+
const feature_states: { [key: number]: FeatureStateModel } = {};
|
|
15
|
+
for (const fs of environment.featureStates) {
|
|
16
|
+
feature_states[fs.feature.id] = fs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Override with any feature states defined by matching segments
|
|
20
|
+
const identity_segments: SegmentModel[] = getIdentitySegments(
|
|
21
|
+
environment,
|
|
22
|
+
identity,
|
|
23
|
+
override_traits
|
|
24
|
+
);
|
|
25
|
+
for (const matching_segment of identity_segments) {
|
|
26
|
+
for (const feature_state of matching_segment.featureStates) {
|
|
27
|
+
// note that feature states are stored on the segment in descending priority
|
|
28
|
+
// order so we only care that the last one is added
|
|
29
|
+
// TODO: can we optimise this?
|
|
30
|
+
feature_states[feature_state.feature.id] = feature_state;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Override with any feature states defined directly the identity
|
|
35
|
+
for (const fs of identity.identityFeatures || []) {
|
|
36
|
+
if (feature_states[fs.feature.id]) {
|
|
37
|
+
feature_states[fs.feature.id] = fs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return feature_states;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getIdentityFeatureState(
|
|
44
|
+
environment: EnvironmentModel,
|
|
45
|
+
identity: IdentityModel,
|
|
46
|
+
feature_name: string,
|
|
47
|
+
override_traits?: TraitModel[]
|
|
48
|
+
): FeatureStateModel {
|
|
49
|
+
const featureStates = getIdentityFeatureStatesDict(environment, identity, override_traits);
|
|
50
|
+
|
|
51
|
+
const matchingFeature = Object.values(featureStates).filter(
|
|
52
|
+
f => f.feature.name === feature_name
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (matchingFeature.length === 0) {
|
|
56
|
+
throw new Error('Feature State Not Found');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return matchingFeature[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getIdentityFeatureStates(
|
|
63
|
+
environment: EnvironmentModel,
|
|
64
|
+
identity: IdentityModel,
|
|
65
|
+
overrideTraits?: TraitModel[]
|
|
66
|
+
): FeatureStateModel[] {
|
|
67
|
+
const feature_states = Object.values(
|
|
68
|
+
getIdentityFeatureStatesDict(environment, identity, overrideTraits)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (environment.project.hideDisabledFlags) {
|
|
72
|
+
return feature_states.filter(fs => !!fs.enabled);
|
|
73
|
+
}
|
|
74
|
+
return feature_states;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getEnvironmentFeatureState(environment: EnvironmentModel, featureName: string) {
|
|
78
|
+
const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName);
|
|
79
|
+
|
|
80
|
+
if (featuresStates.length === 0) {
|
|
81
|
+
throw new Error('Feature State Not Found');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return featuresStates[0];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getEnvironmentFeatureStates(environment: EnvironmentModel): FeatureStateModel[] {
|
|
88
|
+
if (environment.project.hideDisabledFlags) {
|
|
89
|
+
return environment.featureStates.filter(fs => !!fs.enabled);
|
|
90
|
+
}
|
|
91
|
+
return environment.featureStates;
|
|
92
|
+
}
|
|
@@ -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
|
+
feature_analytics: boolean,
|
|
12
|
+
stop_serving_flags: boolean,
|
|
13
|
+
persist_trait_data: boolean
|
|
14
|
+
) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.featureAnalytics = feature_analytics;
|
|
18
|
+
this.stopServingFlags = stop_serving_flags;
|
|
19
|
+
this.persistTraitData = persist_trait_data;
|
|
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,23 @@
|
|
|
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
|
+
// FIXME
|
|
10
|
+
segments: SegmentModel[] = [];
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
id: number,
|
|
14
|
+
name: string,
|
|
15
|
+
hide_disabled_flags: boolean,
|
|
16
|
+
organization: OrganisationModel
|
|
17
|
+
) {
|
|
18
|
+
this.id = id;
|
|
19
|
+
this.name = name;
|
|
20
|
+
this.hideDisabledFlags = hide_disabled_flags;
|
|
21
|
+
this.organisation = organization;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { EnvironmentModel } from '../environments/models';
|
|
2
|
+
import { IdentityModel } from '../identities/models';
|
|
3
|
+
import { TraitModel } from '../identities/traits/models';
|
|
4
|
+
import { getHashedPercentateForObjIds } from '../utils/hashing';
|
|
5
|
+
import { PERCENTAGE_SPLIT } from './constants';
|
|
6
|
+
import { SegmentConditionModel, SegmentModel, SegmentRuleModel } from './models';
|
|
7
|
+
|
|
8
|
+
export function getIdentitySegments(
|
|
9
|
+
environment: EnvironmentModel,
|
|
10
|
+
identity: IdentityModel,
|
|
11
|
+
overrideTraits?: TraitModel[]
|
|
12
|
+
): SegmentModel[] {
|
|
13
|
+
return environment.project.segments.filter(segment =>
|
|
14
|
+
evaluateIdentityInSegment(identity, segment, overrideTraits)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function evaluateIdentityInSegment(
|
|
19
|
+
identity: IdentityModel,
|
|
20
|
+
segment: SegmentModel,
|
|
21
|
+
overrideTraits?: TraitModel[]
|
|
22
|
+
): boolean {
|
|
23
|
+
return (
|
|
24
|
+
segment.rules.length > 0 &&
|
|
25
|
+
segment.rules.filter(rule =>
|
|
26
|
+
traitsMatchSegmentRule(
|
|
27
|
+
overrideTraits || identity.identityTraits,
|
|
28
|
+
rule,
|
|
29
|
+
segment.id,
|
|
30
|
+
identity.compositeKey
|
|
31
|
+
)
|
|
32
|
+
).length === segment.rules.length
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function traitsMatchSegmentRule(
|
|
37
|
+
identityTraits: TraitModel[],
|
|
38
|
+
rule: SegmentRuleModel,
|
|
39
|
+
segmentId: number | string,
|
|
40
|
+
identityId: number | string
|
|
41
|
+
): boolean {
|
|
42
|
+
const matchesConditions =
|
|
43
|
+
rule.conditions.length > 0
|
|
44
|
+
? rule.matchingFunction()(
|
|
45
|
+
rule.conditions.map(condition =>
|
|
46
|
+
traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
: true;
|
|
50
|
+
return (
|
|
51
|
+
matchesConditions &&
|
|
52
|
+
rule.rules.filter(rule =>
|
|
53
|
+
traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId)
|
|
54
|
+
).length === rule.rules.length
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function traitsMatchSegmentCondition(
|
|
59
|
+
identity_traits: TraitModel[],
|
|
60
|
+
condition: SegmentConditionModel,
|
|
61
|
+
segmentId: number | string,
|
|
62
|
+
identityId: number | string
|
|
63
|
+
): boolean {
|
|
64
|
+
if (condition.operator == PERCENTAGE_SPLIT) {
|
|
65
|
+
return getHashedPercentateForObjIds([segmentId, identityId]) <= parseFloat(condition.value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const traits = identity_traits.filter(t => t.traitKey === condition.property_);
|
|
69
|
+
const trait = traits.length > 0 ? traits[0] : undefined;
|
|
70
|
+
|
|
71
|
+
return trait ? condition.matchesTraitValue(trait.traitValue) : false;
|
|
72
|
+
}
|