flagsmith-nodejs 2.0.0-beta.5 → 2.0.0
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 +37 -29
- package/.tool-versions +1 -0
- package/README.md +7 -0
- package/example/package-lock.json +1 -996
- package/flagsmith-engine/features/models.ts +28 -6
- package/flagsmith-engine/features/util.ts +9 -0
- package/flagsmith-engine/identities/models.ts +1 -1
- package/flagsmith-engine/index.ts +7 -5
- package/flagsmith-engine/organisations/models.ts +1 -1
- package/flagsmith-engine/segments/models.ts +21 -4
- package/flagsmith-engine/segments/util.ts +8 -0
- package/flagsmith-engine/utils/collections.ts +1 -12
- package/flagsmith-engine/utils/hashing/index.ts +5 -2
- package/flagsmith-engine/utils/index.ts +6 -2
- package/package.json +4 -1
- package/sdk/analytics.ts +0 -2
- package/sdk/index.ts +84 -28
- package/sdk/polling_manager.ts +0 -1
- package/sdk/types.ts +8 -0
- package/sdk/utils.ts +4 -5
- package/tests/engine/unit/{egine.test.ts → engine.test.ts} +20 -0
- package/tests/engine/unit/features/models.test.ts +8 -4
- package/tests/engine/unit/identities/identities_builders.test.ts +13 -0
- package/tests/engine/unit/identities/identities_models.test.ts +3 -14
- package/tests/engine/unit/organization/models.test.ts +1 -1
- package/tests/engine/unit/segments/segments_model.test.ts +22 -1
- package/tests/engine/unit/utils.ts +1 -1
- package/tests/sdk/analytics.test.ts +11 -0
- package/tests/sdk/flagsmith-cache.test.ts +150 -0
- package/tests/sdk/flagsmith-environment-flags.test.ts +197 -0
- package/tests/sdk/flagsmith-identity-flags.test.ts +140 -0
- package/tests/sdk/flagsmith.test.ts +100 -85
- package/tests/sdk/polling.test.ts +25 -0
- package/tests/sdk/utils.ts +21 -2
- package/tsconfig.json +2 -1
- package/.idea/flagsmith-nodejs-client.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/build/flagsmith-engine/environments/integrations/models.d.ts +0 -4
- package/build/flagsmith-engine/environments/integrations/models.js +0 -8
- package/build/flagsmith-engine/environments/models.d.ts +0 -25
- package/build/flagsmith-engine/environments/models.js +0 -40
- package/build/flagsmith-engine/environments/util.d.ts +0 -3
- package/build/flagsmith-engine/environments/util.js +0 -19
- package/build/flagsmith-engine/features/constants.d.ts +0 -4
- package/build/flagsmith-engine/features/constants.js +0 -7
- package/build/flagsmith-engine/features/models.d.ts +0 -32
- package/build/flagsmith-engine/features/models.js +0 -85
- package/build/flagsmith-engine/features/util.d.ts +0 -3
- package/build/flagsmith-engine/features/util.js +0 -20
- package/build/flagsmith-engine/identities/models.d.ts +0 -15
- package/build/flagsmith-engine/identities/models.js +0 -47
- package/build/flagsmith-engine/identities/traits/models.d.ts +0 -5
- package/build/flagsmith-engine/identities/traits/models.js +0 -12
- package/build/flagsmith-engine/identities/util.d.ts +0 -4
- package/build/flagsmith-engine/identities/util.js +0 -22
- package/build/flagsmith-engine/index.d.ts +0 -8
- package/build/flagsmith-engine/index.js +0 -61
- package/build/flagsmith-engine/organisations/models.d.ts +0 -9
- package/build/flagsmith-engine/organisations/models.js +0 -21
- package/build/flagsmith-engine/organisations/util.d.ts +0 -2
- package/build/flagsmith-engine/organisations/util.js +0 -8
- package/build/flagsmith-engine/projects/models.d.ts +0 -10
- package/build/flagsmith-engine/projects/models.js +0 -17
- package/build/flagsmith-engine/projects/util.d.ts +0 -2
- package/build/flagsmith-engine/projects/util.js +0 -15
- package/build/flagsmith-engine/segments/constants.d.ts +0 -26
- package/build/flagsmith-engine/segments/constants.js +0 -31
- package/build/flagsmith-engine/segments/evaluators.d.ts +0 -6
- package/build/flagsmith-engine/segments/evaluators.js +0 -29
- package/build/flagsmith-engine/segments/models.d.ts +0 -31
- package/build/flagsmith-engine/segments/models.js +0 -83
- package/build/flagsmith-engine/segments/util.d.ts +0 -4
- package/build/flagsmith-engine/segments/util.js +0 -23
- package/build/flagsmith-engine/utils/collections.d.ts +0 -4
- package/build/flagsmith-engine/utils/collections.js +0 -16
- package/build/flagsmith-engine/utils/errors.d.ts +0 -2
- package/build/flagsmith-engine/utils/errors.js +0 -6
- package/build/flagsmith-engine/utils/hashing/index.d.ts +0 -9
- package/build/flagsmith-engine/utils/hashing/index.js +0 -54
- package/build/flagsmith-engine/utils/index.d.ts +0 -1
- package/build/flagsmith-engine/utils/index.js +0 -14
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -11
- package/build/sdk/analytics.d.ts +0 -28
- package/build/sdk/analytics.js +0 -60
- package/build/sdk/errors.d.ts +0 -4
- package/build/sdk/errors.js +0 -9
- package/build/sdk/index.d.ts +0 -118
- package/build/sdk/index.js +0 -263
- package/build/sdk/models.d.ts +0 -55
- package/build/sdk/models.js +0 -101
- package/build/sdk/polling_manager.d.ts +0 -9
- package/build/sdk/polling_manager.js +0 -31
- package/build/sdk/utils.d.ts +0 -12
- package/build/sdk/utils.js +0 -45
- package/tests/engine/engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json +0 -12393
- package/tests/engine/engine-tests/engine-test-data/readme.md +0 -30
|
@@ -51,6 +51,7 @@ export class FeatureStateModel {
|
|
|
51
51
|
enabled: boolean;
|
|
52
52
|
djangoID: number;
|
|
53
53
|
featurestateUUID: string = uuidv4();
|
|
54
|
+
featureSegment?: FeatureSegment;
|
|
54
55
|
private value: any;
|
|
55
56
|
multivariateFeatureStateValues: MultivariateFeatureStateValueModel[] = [];
|
|
56
57
|
|
|
@@ -79,10 +80,23 @@ export class FeatureStateModel {
|
|
|
79
80
|
return this.value;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
/*
|
|
84
|
+
Returns `True` if `this` is higher segment priority than `other`
|
|
85
|
+
(i.e. has lower value for featureSegment.priority)
|
|
86
|
+
NOTE:
|
|
87
|
+
A segment will be considered higher priority only if:
|
|
88
|
+
1. `other` does not have a feature segment(i.e: it is an environment feature state or it's a
|
|
89
|
+
feature state with feature segment but from an old document that does not have `featureSegment.priority`)
|
|
90
|
+
but `this` does.
|
|
91
|
+
2. `other` have a feature segment with high priority
|
|
92
|
+
*/
|
|
93
|
+
isHigherSegmentPriority(other: FeatureStateModel): boolean {
|
|
94
|
+
if (!other.featureSegment || !this.featureSegment) {
|
|
95
|
+
return !!this.featureSegment && !other.featureSegment;
|
|
96
|
+
}
|
|
97
|
+
return this.featureSegment.priority < other.featureSegment.priority;
|
|
84
98
|
}
|
|
85
|
-
|
|
99
|
+
|
|
86
100
|
getMultivariateValue(identityID: number | string) {
|
|
87
101
|
const percentageValue = getHashedPercentateForObjIds([
|
|
88
102
|
this.djangoID || this.featurestateUUID,
|
|
@@ -90,9 +104,9 @@ export class FeatureStateModel {
|
|
|
90
104
|
]);
|
|
91
105
|
|
|
92
106
|
let startPercentage = 0;
|
|
93
|
-
const sortedF = this.multivariateFeatureStateValues.sort((a, b) =>
|
|
94
|
-
|
|
95
|
-
);
|
|
107
|
+
const sortedF = this.multivariateFeatureStateValues.sort((a, b) =>{
|
|
108
|
+
return a.id - b.id;
|
|
109
|
+
});
|
|
96
110
|
for (const myValue of sortedF) {
|
|
97
111
|
const limit = myValue.percentageAllocation + startPercentage;
|
|
98
112
|
if (startPercentage <= percentageValue && percentageValue < limit) {
|
|
@@ -103,3 +117,11 @@ export class FeatureStateModel {
|
|
|
103
117
|
return this.value;
|
|
104
118
|
}
|
|
105
119
|
}
|
|
120
|
+
|
|
121
|
+
export class FeatureSegment {
|
|
122
|
+
priority: number;
|
|
123
|
+
|
|
124
|
+
constructor(priority: number) {
|
|
125
|
+
this.priority = priority;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FeatureModel,
|
|
3
|
+
FeatureSegment,
|
|
3
4
|
FeatureStateModel,
|
|
4
5
|
MultivariateFeatureOptionModel,
|
|
5
6
|
MultivariateFeatureStateValueModel
|
|
@@ -18,6 +19,10 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
|
|
|
18
19
|
featuresStateModelJSON.uuid
|
|
19
20
|
);
|
|
20
21
|
|
|
22
|
+
featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
|
|
23
|
+
buildFeatureSegment(featuresStateModelJSON.feature_segment) :
|
|
24
|
+
undefined;
|
|
25
|
+
|
|
21
26
|
const multivariateFeatureStateValues = featuresStateModelJSON.multivariate_feature_state_values
|
|
22
27
|
? featuresStateModelJSON.multivariate_feature_state_values.map((fsv: any) => {
|
|
23
28
|
const featureOption = new MultivariateFeatureOptionModel(
|
|
@@ -36,3 +41,7 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
|
|
|
36
41
|
|
|
37
42
|
return featureStateModel;
|
|
38
43
|
}
|
|
44
|
+
|
|
45
|
+
export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment {
|
|
46
|
+
return new FeatureSegment(featureSegmentJSON.priority);
|
|
47
|
+
}
|
|
@@ -37,7 +37,7 @@ export class IdentityModel {
|
|
|
37
37
|
return `${env_key}_${identifier}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
updateTraits(traits: TraitModel[]) {
|
|
41
41
|
const existingTraits: Map<string, TraitModel> = new Map();
|
|
42
42
|
for (const trait of this.identityTraits) {
|
|
43
43
|
existingTraits.set(trait.traitKey, trait);
|
|
@@ -25,15 +25,17 @@ function getIdentityFeatureStatesDict(
|
|
|
25
25
|
);
|
|
26
26
|
for (const matchingSegment of identitySegments) {
|
|
27
27
|
for (const featureState of matchingSegment.featureStates) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
if (featureStates[featureState.feature.id]) {
|
|
29
|
+
if (featureStates[featureState.feature.id].isHigherSegmentPriority(featureState)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
31
33
|
featureStates[featureState.feature.id] = featureState;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Override with any feature states defined directly the identity
|
|
36
|
-
for (const fs of identity.identityFeatures
|
|
38
|
+
for (const fs of identity.identityFeatures) {
|
|
37
39
|
if (featureStates[fs.feature.id]) {
|
|
38
40
|
featureStates[fs.feature.id] = fs;
|
|
39
41
|
}
|
|
@@ -79,7 +81,7 @@ export function getEnvironmentFeatureState(environment: EnvironmentModel, featur
|
|
|
79
81
|
const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName);
|
|
80
82
|
|
|
81
83
|
if (featuresStates.length === 0) {
|
|
82
|
-
throw new
|
|
84
|
+
throw new FeatureStateNotFound('Feature State Not Found');
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
return featuresStates[0];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
1
3
|
import { FeatureStateModel } from '../features/models';
|
|
2
4
|
import { getCastingFunction as getCastingFunction } from '../utils';
|
|
3
5
|
import {
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
REGEX,
|
|
9
11
|
CONDITION_OPERATORS
|
|
10
12
|
} from './constants';
|
|
13
|
+
import { isSemver } from './util';
|
|
11
14
|
|
|
12
15
|
export const all = (iterable: Array<any>) => iterable.filter(e => !!e).length === iterable.length;
|
|
13
16
|
export const any = (iterable: Array<any>) => iterable.filter(e => !!e).length > 0;
|
|
@@ -23,10 +26,21 @@ export const matchingFunctions = {
|
|
|
23
26
|
[CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
|
|
24
27
|
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
|
|
25
28
|
otherValue.includes(thisValue),
|
|
26
|
-
[CONDITION_OPERATORS.NOT_CONTAINS]: (thisValue: any, otherValue: any) =>
|
|
27
|
-
!otherValue.includes(thisValue)
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
export const semverMatchingFunction = {
|
|
32
|
+
...matchingFunctions,
|
|
33
|
+
[CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => semver.eq(thisValue, otherValue),
|
|
34
|
+
[CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => semver.gt(otherValue, thisValue),
|
|
35
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
|
|
36
|
+
semver.gte(otherValue, thisValue),
|
|
37
|
+
[CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => semver.gt(thisValue, otherValue),
|
|
38
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
|
|
39
|
+
semver.gte(thisValue, otherValue),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const getMatchingFunctions = (semver: boolean) => (semver ? semverMatchingFunction : matchingFunctions);
|
|
43
|
+
|
|
30
44
|
export class SegmentConditionModel {
|
|
31
45
|
EXCEPTION_OPERATOR_METHODS: { [key: string]: string } = {
|
|
32
46
|
[NOT_CONTAINS]: 'evaluateNotContains',
|
|
@@ -61,9 +75,12 @@ export class SegmentConditionModel {
|
|
|
61
75
|
|
|
62
76
|
const defaultFunction = (x: any, y: any) => false;
|
|
63
77
|
|
|
64
|
-
const
|
|
78
|
+
const matchingFunctionSet = getMatchingFunctions(isSemver(this.value));
|
|
79
|
+
const matchingFunction = matchingFunctionSet[this.operator] || defaultFunction;
|
|
80
|
+
|
|
81
|
+
const traitType = isSemver(this.value) ? 'semver' : typeof traitValue;
|
|
82
|
+
const castToTypeOfTraitValue = getCastingFunction(traitType);
|
|
65
83
|
|
|
66
|
-
const castToTypeOfTraitValue = getCastingFunction(traitValue);
|
|
67
84
|
return matchingFunction(castToTypeOfTraitValue(this.value), traitValue);
|
|
68
85
|
}
|
|
69
86
|
}
|
|
@@ -27,3 +27,11 @@ export function buildSegmentModel(segmentModelJSON: any): SegmentModel {
|
|
|
27
27
|
|
|
28
28
|
return model;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export function isSemver(value: any) {
|
|
32
|
+
return typeof value == 'string' && value.endsWith(':semver');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function removeSemverSuffix(value: string) {
|
|
36
|
+
return value.replace(':semver', '');
|
|
37
|
+
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
1
|
import { FeatureStateModel } from '../features/models';
|
|
2
2
|
|
|
3
|
-
export class IdentityFeaturesList extends Array<FeatureStateModel> {
|
|
4
|
-
public push(...e: FeatureStateModel[]): number {
|
|
5
|
-
for (const [_, item] of e.entries()) {
|
|
6
|
-
for (const [k, v] of this.entries()) {
|
|
7
|
-
if (v.djangoID === item.djangoID) {
|
|
8
|
-
throw new Error('feature state for this feature already exists');
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
return super.push(...e);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
3
|
+
export class IdentityFeaturesList extends Array<FeatureStateModel> {}
|
|
@@ -39,12 +39,15 @@ function h2d(s: any): string {
|
|
|
39
39
|
* @returns number number between 0 (inclusive) and 100 (exclusive)
|
|
40
40
|
*/
|
|
41
41
|
export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations = 1): number {
|
|
42
|
-
let
|
|
43
|
-
const hashedValue = md5(
|
|
42
|
+
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
43
|
+
const hashedValue = md5(toHash);
|
|
44
44
|
const hashedInt = bigInt(h2d(hashedValue));
|
|
45
45
|
const value = (hashedInt.mod(9999).toJSNumber() / 9998) * 100;
|
|
46
46
|
|
|
47
|
+
// we ignore this for it's nearly impossible use case to catch
|
|
48
|
+
/* istanbul ignore next */
|
|
47
49
|
if (value === 100) {
|
|
50
|
+
/* istanbul ignore next */
|
|
48
51
|
return getHashedPercentateForObjIds(objectIds, iterations + 1);
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { removeSemverSuffix } from "../segments/util";
|
|
2
|
+
|
|
3
|
+
export function getCastingFunction(traitType: 'boolean' | 'string' | 'number' | 'semver' | any): CallableFunction {
|
|
4
|
+
switch (traitType) {
|
|
3
5
|
case 'boolean':
|
|
4
6
|
return (x: any) => !['False', 'false'].includes(x);
|
|
5
7
|
case 'number':
|
|
6
8
|
return (x: any) => parseFloat(x);
|
|
9
|
+
case 'semver':
|
|
10
|
+
return (x: any) => removeSemverSuffix(x);
|
|
7
11
|
default:
|
|
8
12
|
return (x: any) => String(x);
|
|
9
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -46,18 +46,21 @@
|
|
|
46
46
|
"test:watch": "jest --coverage --watch --coverageReporters='text'",
|
|
47
47
|
"test:debug": "node --inspect-brk node_modules/.bin/jest --coverage --watch --coverageReporters='text'",
|
|
48
48
|
"build": "tsc",
|
|
49
|
+
"prepublish": "npm run build",
|
|
49
50
|
"prepare": "husky install"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"big-integer": "^1.6.51",
|
|
53
54
|
"md5": "^2.3.0",
|
|
54
55
|
"node-fetch": "^2.1.2",
|
|
56
|
+
"semver": "^7.3.7",
|
|
55
57
|
"uuid": "^8.3.2"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@types/jest": "^27.4.1",
|
|
59
61
|
"@types/md5": "^2.3.2",
|
|
60
62
|
"@types/node-fetch": "^2.6.1",
|
|
63
|
+
"@types/semver": "^7.3.9",
|
|
61
64
|
"@types/uuid": "^8.3.4",
|
|
62
65
|
"esbuild": "^0.14.25",
|
|
63
66
|
"husky": "^7.0.4",
|
package/sdk/analytics.ts
CHANGED
|
@@ -5,8 +5,6 @@ const ANALYTICS_ENDPOINT = 'analytics/flags/';
|
|
|
5
5
|
// Used to control how often we send data(in seconds)
|
|
6
6
|
const ANALYTICS_TIMER = 10;
|
|
7
7
|
|
|
8
|
-
const delay = (ms: number) => new Promise(resolve => setTimeout(() => resolve(undefined), ms));
|
|
9
|
-
|
|
10
8
|
export class AnalyticsProcessor {
|
|
11
9
|
private analyticsEndpoint: string;
|
|
12
10
|
private environmentKey: string;
|
package/sdk/index.ts
CHANGED
|
@@ -12,8 +12,9 @@ import { EnvironmentDataPollingManager } from './polling_manager';
|
|
|
12
12
|
import { generateIdentitiesData, retryFetch } from './utils';
|
|
13
13
|
import { SegmentModel } from '../flagsmith-engine/segments/models';
|
|
14
14
|
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators';
|
|
15
|
+
import { FlagsmithCache } from './types';
|
|
15
16
|
|
|
16
|
-
const DEFAULT_API_URL = 'https://api.flagsmith.com/api/v1/';
|
|
17
|
+
const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
|
|
17
18
|
|
|
18
19
|
export class Flagsmith {
|
|
19
20
|
environmentKey?: string;
|
|
@@ -26,12 +27,16 @@ export class Flagsmith {
|
|
|
26
27
|
enableAnalytics: boolean = false;
|
|
27
28
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
28
29
|
|
|
30
|
+
|
|
29
31
|
environmentFlagsUrl: string;
|
|
30
32
|
identitiesUrl: string;
|
|
31
33
|
environmentUrl: string;
|
|
32
34
|
|
|
33
35
|
environmentDataPollingManager?: EnvironmentDataPollingManager;
|
|
34
36
|
environment!: EnvironmentModel;
|
|
37
|
+
|
|
38
|
+
private cache?: FlagsmithCache;
|
|
39
|
+
private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
|
|
35
40
|
private analyticsProcessor?: AnalyticsProcessor;
|
|
36
41
|
/**
|
|
37
42
|
* A Flagsmith client.
|
|
@@ -73,6 +78,8 @@ export class Flagsmith {
|
|
|
73
78
|
retries?: number;
|
|
74
79
|
enableAnalytics?: boolean;
|
|
75
80
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
81
|
+
cache?: FlagsmithCache,
|
|
82
|
+
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void,
|
|
76
83
|
}) {
|
|
77
84
|
this.environmentKey = data.environmentKey;
|
|
78
85
|
this.apiUrl = data.apiUrl || this.apiUrl;
|
|
@@ -88,6 +95,20 @@ export class Flagsmith {
|
|
|
88
95
|
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
|
|
89
96
|
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
90
97
|
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
98
|
+
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
99
|
+
|
|
100
|
+
if (!!data.cache) {
|
|
101
|
+
const missingMethods: string[] = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
|
|
102
|
+
|
|
103
|
+
if (missingMethods.length > 0) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Please implement the following methods in your cache: ${missingMethods.join(
|
|
106
|
+
', '
|
|
107
|
+
)}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
this.cache = data.cache;
|
|
111
|
+
}
|
|
91
112
|
|
|
92
113
|
if (this.enableLocalEvaluation) {
|
|
93
114
|
if (!this.environmentKey.startsWith('ser.')) {
|
|
@@ -105,10 +126,10 @@ export class Flagsmith {
|
|
|
105
126
|
|
|
106
127
|
this.analyticsProcessor = data.enableAnalytics
|
|
107
128
|
? new AnalyticsProcessor({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
environmentKey: this.environmentKey,
|
|
130
|
+
baseApiUrl: this.apiUrl,
|
|
131
|
+
timeout: this.requestTimeoutSeconds
|
|
132
|
+
})
|
|
112
133
|
: undefined;
|
|
113
134
|
}
|
|
114
135
|
/**
|
|
@@ -117,8 +138,19 @@ export class Flagsmith {
|
|
|
117
138
|
* @returns Flags object holding all the flags for the current environment.
|
|
118
139
|
*/
|
|
119
140
|
async getEnvironmentFlags(): Promise<Flags> {
|
|
141
|
+
const cachedItem = !!this.cache && await this.cache.get(`flags`);
|
|
142
|
+
if (!!cachedItem) {
|
|
143
|
+
return cachedItem;
|
|
144
|
+
}
|
|
145
|
+
if (this.enableLocalEvaluation) {
|
|
146
|
+
return new Promise(resolve =>
|
|
147
|
+
this.environmentPromise!.then(() => {
|
|
148
|
+
resolve(this.getEnvironmentFlagsFromDocument());
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
}
|
|
120
152
|
if (this.environment) {
|
|
121
|
-
return
|
|
153
|
+
return this.getEnvironmentFlagsFromDocument();
|
|
122
154
|
}
|
|
123
155
|
|
|
124
156
|
return this.getEnvironmentFlagsFromApi();
|
|
@@ -134,7 +166,11 @@ export class Flagsmith {
|
|
|
134
166
|
Flagsmith, e.g. {"num_orders": 10}
|
|
135
167
|
* @returns Flags object holding all the flags for the given identity.
|
|
136
168
|
*/
|
|
137
|
-
getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
|
|
169
|
+
async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
|
|
170
|
+
const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
|
|
171
|
+
if (!!cachedItem) {
|
|
172
|
+
return cachedItem;
|
|
173
|
+
}
|
|
138
174
|
traits = traits || {};
|
|
139
175
|
if (this.enableLocalEvaluation) {
|
|
140
176
|
return new Promise(resolve =>
|
|
@@ -188,14 +224,23 @@ export class Flagsmith {
|
|
|
188
224
|
* You only need to call this if you wish to bypass environmentRefreshIntervalSeconds.
|
|
189
225
|
*/
|
|
190
226
|
async updateEnvironment() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.environmentPromise
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
227
|
+
try {
|
|
228
|
+
const request = this.getEnvironmentFromApi();
|
|
229
|
+
if (!this.environmentPromise) {
|
|
230
|
+
this.environmentPromise = request.then(res => {
|
|
231
|
+
this.environment = res;
|
|
232
|
+
});
|
|
233
|
+
await this.environmentPromise;
|
|
234
|
+
} else {
|
|
235
|
+
this.environment = await request;
|
|
236
|
+
}
|
|
237
|
+
if (this.onEnvironmentChange) {
|
|
238
|
+
this.onEnvironmentChange(null, this.environment);
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (this.onEnvironmentChange) {
|
|
242
|
+
this.onEnvironmentChange(e as Error, this.environment);
|
|
243
|
+
}
|
|
199
244
|
}
|
|
200
245
|
}
|
|
201
246
|
|
|
@@ -224,7 +269,6 @@ export class Flagsmith {
|
|
|
224
269
|
headers: headers
|
|
225
270
|
},
|
|
226
271
|
this.retries,
|
|
227
|
-
1000,
|
|
228
272
|
(this.requestTimeoutSeconds || 10) * 1000
|
|
229
273
|
);
|
|
230
274
|
|
|
@@ -247,15 +291,19 @@ export class Flagsmith {
|
|
|
247
291
|
return buildEnvironmentModel(environment_data);
|
|
248
292
|
}
|
|
249
293
|
|
|
250
|
-
private getEnvironmentFlagsFromDocument() {
|
|
251
|
-
|
|
294
|
+
private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
|
|
295
|
+
const flags = Flags.fromFeatureStateModels({
|
|
252
296
|
featureStates: getEnvironmentFeatureStates(this.environment),
|
|
253
297
|
analyticsProcessor: this.analyticsProcessor,
|
|
254
298
|
defaultFlagHandler: this.defaultFlagHandler
|
|
255
299
|
});
|
|
300
|
+
if (!!this.cache) {
|
|
301
|
+
await this.cache.set('flags', flags);
|
|
302
|
+
}
|
|
303
|
+
return flags;
|
|
256
304
|
}
|
|
257
305
|
|
|
258
|
-
private getIdentityFlagsFromDocument(identifier: string, traits: { [key: string]: any }) {
|
|
306
|
+
private async getIdentityFlagsFromDocument(identifier: string, traits: { [key: string]: any }): Promise<Flags> {
|
|
259
307
|
const identityModel = this.buildIdentityModel(
|
|
260
308
|
identifier,
|
|
261
309
|
Object.keys(traits).map(key => ({
|
|
@@ -266,21 +314,31 @@ export class Flagsmith {
|
|
|
266
314
|
|
|
267
315
|
const featureStates = getIdentityFeatureStates(this.environment, identityModel);
|
|
268
316
|
|
|
269
|
-
|
|
317
|
+
const flags = Flags.fromFeatureStateModels({
|
|
270
318
|
featureStates: featureStates,
|
|
271
319
|
analyticsProcessor: this.analyticsProcessor,
|
|
272
320
|
defaultFlagHandler: this.defaultFlagHandler
|
|
273
321
|
});
|
|
322
|
+
|
|
323
|
+
if (!!this.cache) {
|
|
324
|
+
await this.cache.set(`flags-${identifier}`, flags);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return flags;
|
|
274
328
|
}
|
|
275
329
|
|
|
276
330
|
private async getEnvironmentFlagsFromApi() {
|
|
277
331
|
try {
|
|
278
332
|
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
279
|
-
|
|
333
|
+
const flags = Flags.fromAPIFlags({
|
|
280
334
|
apiFlags: apiFlags,
|
|
281
335
|
analyticsProcessor: this.analyticsProcessor,
|
|
282
336
|
defaultFlagHandler: this.defaultFlagHandler
|
|
283
337
|
});
|
|
338
|
+
if (!!this.cache) {
|
|
339
|
+
await this.cache.set('flags', flags);
|
|
340
|
+
}
|
|
341
|
+
return flags;
|
|
284
342
|
} catch (e) {
|
|
285
343
|
if (this.defaultFlagHandler) {
|
|
286
344
|
return new Flags({
|
|
@@ -297,11 +355,15 @@ export class Flagsmith {
|
|
|
297
355
|
try {
|
|
298
356
|
const data = generateIdentitiesData(identifier, traits);
|
|
299
357
|
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
300
|
-
|
|
358
|
+
const flags = Flags.fromAPIFlags({
|
|
301
359
|
apiFlags: jsonResponse['flags'],
|
|
302
360
|
analyticsProcessor: this.analyticsProcessor,
|
|
303
361
|
defaultFlagHandler: this.defaultFlagHandler
|
|
304
362
|
});
|
|
363
|
+
if (!!this.cache) {
|
|
364
|
+
await this.cache.set(`flags-${identifier}`, flags);
|
|
365
|
+
}
|
|
366
|
+
return flags;
|
|
305
367
|
} catch (e) {
|
|
306
368
|
if (this.defaultFlagHandler) {
|
|
307
369
|
return new Flags({
|
|
@@ -315,12 +377,6 @@ export class Flagsmith {
|
|
|
315
377
|
}
|
|
316
378
|
|
|
317
379
|
private buildIdentityModel(identifier: string, traits: { key: string; value: any }[]) {
|
|
318
|
-
if (!this.environment) {
|
|
319
|
-
throw new FlagsmithClientError(
|
|
320
|
-
'Unable to build identity model when no local environment present.'
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
380
|
const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value));
|
|
325
381
|
return new IdentityModel('0', traitModels, [], this.environment.apiKey, identifier);
|
|
326
382
|
}
|
package/sdk/polling_manager.ts
CHANGED
|
@@ -17,7 +17,6 @@ export class EnvironmentDataPollingManager {
|
|
|
17
17
|
await this.main.updateEnvironment();
|
|
18
18
|
}, this.refreshIntervalSeconds * 1000);
|
|
19
19
|
};
|
|
20
|
-
// todo: this call should be awaited for getIdentityFlags/getEnvironmentFlags when enableLocalEvaluation is true
|
|
21
20
|
this.main.updateEnvironment();
|
|
22
21
|
updateEnvironment();
|
|
23
22
|
}
|
package/sdk/types.ts
ADDED
package/sdk/utils.ts
CHANGED
|
@@ -2,8 +2,8 @@ import fetch, { Response } from 'node-fetch';
|
|
|
2
2
|
// @ts-ignore
|
|
3
3
|
if (typeof fetch.default !== 'undefined') fetch = fetch.default;
|
|
4
4
|
|
|
5
|
-
export function generateIdentitiesData(identifier: string, traits
|
|
6
|
-
const traitsGenerated = Object.entries(traits
|
|
5
|
+
export function generateIdentitiesData(identifier: string, traits: { [key: string]: any }) {
|
|
6
|
+
const traitsGenerated = Object.entries(traits).map(trait => ({
|
|
7
7
|
trait_key: trait[0],
|
|
8
8
|
trait_value: trait[1]
|
|
9
9
|
}));
|
|
@@ -18,9 +18,8 @@ export const delay = (ms: number) =>
|
|
|
18
18
|
|
|
19
19
|
export const retryFetch = (
|
|
20
20
|
url: string,
|
|
21
|
-
fetchOptions
|
|
21
|
+
fetchOptions: any,
|
|
22
22
|
retries = 3,
|
|
23
|
-
retryDelay = 1000,
|
|
24
23
|
timeout: number
|
|
25
24
|
): Promise<Response> => {
|
|
26
25
|
return new Promise((resolve, reject) => {
|
|
@@ -32,7 +31,7 @@ export const retryFetch = (
|
|
|
32
31
|
.then(res => resolve(res))
|
|
33
32
|
.catch(async err => {
|
|
34
33
|
if (n > 0) {
|
|
35
|
-
await delay(
|
|
34
|
+
await delay(1000);
|
|
36
35
|
wrapper(--n);
|
|
37
36
|
} else {
|
|
38
37
|
reject(err);
|
|
@@ -25,6 +25,12 @@ test('test_identity_get_feature_state_without_any_override', () => {
|
|
|
25
25
|
expect(feature_state.feature).toStrictEqual(feature1());
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
test('test_identity_get_feature_state_without_any_override_no_fs', () => {
|
|
29
|
+
expect(() => {
|
|
30
|
+
getIdentityFeatureState(environment(), identity(), 'nonExistentName');
|
|
31
|
+
}).toThrowError('Feature State Not Found');
|
|
32
|
+
});
|
|
33
|
+
|
|
28
34
|
test('test_identity_get_all_feature_states_no_segments', () => {
|
|
29
35
|
const env = environment();
|
|
30
36
|
const ident = identity();
|
|
@@ -61,6 +67,20 @@ test('test_identity_get_all_feature_states_with_traits', () => {
|
|
|
61
67
|
expect(featureStates[0].getValue()).toBe('segment_override');
|
|
62
68
|
});
|
|
63
69
|
|
|
70
|
+
test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => {
|
|
71
|
+
const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue);
|
|
72
|
+
|
|
73
|
+
const env = environmentWithSegmentOverride();
|
|
74
|
+
env.project.hideDisabledFlags = true;
|
|
75
|
+
|
|
76
|
+
const featureStates = getIdentityFeatureStates(
|
|
77
|
+
env,
|
|
78
|
+
identityInSegment(),
|
|
79
|
+
[trait_models]
|
|
80
|
+
);
|
|
81
|
+
expect(featureStates.length).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
64
84
|
test('test_environment_get_all_feature_states', () => {
|
|
65
85
|
const env = environment();
|
|
66
86
|
const featureStates = getEnvironmentFeatureStates(env);
|
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
} from '../../../../flagsmith-engine/features/models';
|
|
8
8
|
import { feature1 } from '../utils';
|
|
9
9
|
|
|
10
|
+
test('test_compare_feature_model', () => {
|
|
11
|
+
const fm1 = new FeatureModel(1, 'a', 'test');
|
|
12
|
+
const fm2 = new FeatureModel(1, 'a', 'test');
|
|
13
|
+
expect(fm1.eq(fm2)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
10
16
|
test('test_initializing_feature_state_creates_default_feature_state_uuid', () => {
|
|
11
17
|
const featureState = new FeatureStateModel(feature1(), true, 1);
|
|
12
18
|
expect(featureState.featurestateUUID).toBeDefined();
|
|
@@ -60,13 +66,11 @@ test('test_feature_state_get_value_mv_values', () => {
|
|
|
60
66
|
const mvFeatureState = new FeatureStateModel(myFeature, true, 1);
|
|
61
67
|
mvFeatureState.multivariateFeatureStateValues = [
|
|
62
68
|
mvFeatureStateValue1,
|
|
63
|
-
mvFeatureStateValue2
|
|
69
|
+
mvFeatureStateValue2,
|
|
64
70
|
];
|
|
65
71
|
|
|
66
72
|
mvFeatureState.setValue(mvFeatureControlValue);
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
// expect(mvFeatureState.getValue(3)).toBe(testCase[1]);
|
|
70
|
-
expect(1).toBe(1);
|
|
74
|
+
expect(mvFeatureState.getValue("test")).toBe(mvFeatureValue2);
|
|
71
75
|
}
|
|
72
76
|
});
|
|
@@ -42,6 +42,19 @@ test('test_build_identity_model_from_dictionary_uses_identity_feature_list_for_i
|
|
|
42
42
|
expect(identityModel.identityFeatures?.length).toBe(1);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test('test_build_identity_model_from_dictionary_uses_identity_feature_list_for_identity_features', () => {
|
|
46
|
+
const identity_dict = {
|
|
47
|
+
id: 1,
|
|
48
|
+
identifier: 'test-identity',
|
|
49
|
+
environment_api_key: 'api-key',
|
|
50
|
+
created_date: '2021-08-22T06:25:23.406995Z',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const identityModel = buildIdentityModel(identity_dict);
|
|
54
|
+
|
|
55
|
+
expect(identityModel.identityFeatures?.length).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
45
58
|
test('test_build_build_identity_model_from_dict_creates_identity_uuid', () => {
|
|
46
59
|
const identity_model = buildIdentityModel({
|
|
47
60
|
identifier: 'test_user',
|