flagsmith-nodejs 6.2.0 → 7.0.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.
- package/.github/workflows/publish.yml +8 -5
- package/.github/workflows/pull_request.yaml +3 -0
- package/.gitmodules +1 -1
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +49 -0
- package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/cjs/flagsmith-engine/environments/models.js +3 -1
- package/build/cjs/flagsmith-engine/environments/util.js +1 -1
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
- package/build/cjs/flagsmith-engine/features/models.js +1 -1
- package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
- package/build/cjs/flagsmith-engine/features/types.js +9 -0
- package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
- package/build/cjs/flagsmith-engine/features/util.js +5 -1
- package/build/cjs/flagsmith-engine/index.d.ts +61 -9
- package/build/cjs/flagsmith-engine/index.js +176 -56
- package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
- package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
- package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/cjs/flagsmith-engine/segments/models.js +115 -13
- package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
- package/build/cjs/sdk/index.d.ts +1 -3
- package/build/cjs/sdk/index.js +22 -19
- package/build/cjs/sdk/models.d.ts +8 -1
- package/build/cjs/sdk/models.js +29 -1
- package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/esm/flagsmith-engine/environments/models.js +3 -1
- package/build/esm/flagsmith-engine/environments/util.js +1 -1
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
- package/build/esm/flagsmith-engine/features/models.js +2 -2
- package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
- package/build/esm/flagsmith-engine/features/types.js +6 -0
- package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
- package/build/esm/flagsmith-engine/features/util.js +3 -0
- package/build/esm/flagsmith-engine/index.d.ts +61 -9
- package/build/esm/flagsmith-engine/index.js +161 -43
- package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/esm/flagsmith-engine/segments/constants.js +1 -0
- package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
- package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/esm/flagsmith-engine/segments/models.js +115 -13
- package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
- package/build/esm/sdk/index.d.ts +1 -3
- package/build/esm/sdk/index.js +21 -18
- package/build/esm/sdk/models.d.ts +8 -1
- package/build/esm/sdk/models.js +29 -1
- package/flagsmith-engine/environments/models.ts +3 -1
- package/flagsmith-engine/environments/util.ts +2 -1
- package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
- package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
- package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
- package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
- package/flagsmith-engine/evaluation/models.ts +96 -0
- package/flagsmith-engine/features/models.ts +3 -2
- package/flagsmith-engine/features/types.ts +5 -0
- package/flagsmith-engine/features/util.ts +4 -0
- package/flagsmith-engine/index.ts +229 -72
- package/flagsmith-engine/segments/constants.ts +1 -0
- package/flagsmith-engine/segments/evaluators.ts +178 -62
- package/flagsmith-engine/segments/models.ts +171 -23
- package/flagsmith-engine/utils/hashing/index.ts +2 -2
- package/package.json +13 -2
- package/sdk/index.ts +36 -23
- package/sdk/models.ts +44 -2
- package/tests/engine/e2e/engine.test.ts +43 -38
- package/tests/engine/unit/engine.test.ts +306 -59
- package/tests/engine/unit/mappers.test.ts +353 -0
- package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
- package/tests/engine/unit/segments/segments_model.test.ts +85 -0
- package/tests/engine/unit/utils/utils.test.ts +7 -7
- package/tests/engine/unit/utils.ts +1 -1
- package/tests/sdk/data/environment.json +1 -0
- package/tests/sdk/flagsmith.test.ts +29 -3
- package/tests/sdk/offline-handlers.test.ts +3 -1
- package/vitest.config.esm.ts +34 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as semver from 'semver';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FeatureModel,
|
|
5
|
+
FeatureStateModel,
|
|
6
|
+
MultivariateFeatureOptionModel,
|
|
7
|
+
MultivariateFeatureStateValueModel
|
|
8
|
+
} from '../features/models.js';
|
|
4
9
|
import { getCastingFunction as getCastingFunction } from '../utils/index.js';
|
|
5
10
|
import {
|
|
6
11
|
ALL_RULE,
|
|
@@ -13,6 +18,12 @@ import {
|
|
|
13
18
|
CONDITION_OPERATORS
|
|
14
19
|
} from './constants.js';
|
|
15
20
|
import { isSemver } from './util.js';
|
|
21
|
+
import {
|
|
22
|
+
EvaluationContext,
|
|
23
|
+
Overrides
|
|
24
|
+
} from '../evaluation/evaluationContext/evaluationContext.types.js';
|
|
25
|
+
import { CONSTANTS } from '../features/constants.js';
|
|
26
|
+
import { EvaluationResultSegments, SegmentSource } from '../evaluation/models.js';
|
|
16
27
|
|
|
17
28
|
export const all = (iterable: Array<any>) => iterable.filter(e => !!e).length === iterable.length;
|
|
18
29
|
export const any = (iterable: Array<any>) => iterable.filter(e => !!e).length > 0;
|
|
@@ -26,22 +37,45 @@ export const matchingFunctions = {
|
|
|
26
37
|
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
|
|
27
38
|
thisValue >= otherValue,
|
|
28
39
|
[CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
|
|
29
|
-
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
|
|
30
|
-
|
|
40
|
+
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) => {
|
|
41
|
+
try {
|
|
42
|
+
return !!otherValue && otherValue.includes(thisValue);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Semver library throws an error if the version is invalid, in this case, we want to catch and return false
|
|
50
|
+
const safeSemverCompare = (
|
|
51
|
+
semverMatchingFunction: (conditionValue: any, traitValue: any) => boolean
|
|
52
|
+
) => {
|
|
53
|
+
return (conditionValue: any, traitValue: any) => {
|
|
54
|
+
try {
|
|
55
|
+
return semverMatchingFunction(conditionValue, traitValue);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
31
60
|
};
|
|
32
61
|
|
|
33
62
|
export const semverMatchingFunction = {
|
|
34
63
|
...matchingFunctions,
|
|
35
|
-
[CONDITION_OPERATORS.EQUAL]: (
|
|
36
|
-
semver.eq(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[CONDITION_OPERATORS.
|
|
42
|
-
semver.
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
[CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) =>
|
|
65
|
+
semver.eq(traitValue, conditionValue)
|
|
66
|
+
),
|
|
67
|
+
[CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) =>
|
|
68
|
+
semver.gt(traitValue, conditionValue)
|
|
69
|
+
),
|
|
70
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
|
|
71
|
+
semver.gte(traitValue, conditionValue)
|
|
72
|
+
),
|
|
73
|
+
[CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) =>
|
|
74
|
+
semver.lt(traitValue, conditionValue)
|
|
75
|
+
),
|
|
76
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
|
|
77
|
+
semver.lte(traitValue, conditionValue)
|
|
78
|
+
)
|
|
45
79
|
};
|
|
46
80
|
|
|
47
81
|
export const getMatchingFunctions = (semver: boolean) =>
|
|
@@ -56,17 +90,17 @@ export class SegmentConditionModel {
|
|
|
56
90
|
};
|
|
57
91
|
|
|
58
92
|
operator: string;
|
|
59
|
-
value: string | null | undefined;
|
|
60
|
-
|
|
93
|
+
value: string | null | undefined | string[];
|
|
94
|
+
property: string | null | undefined;
|
|
61
95
|
|
|
62
96
|
constructor(
|
|
63
97
|
operator: string,
|
|
64
|
-
value?: string | null | undefined,
|
|
98
|
+
value?: string | null | undefined | string[],
|
|
65
99
|
property?: string | null | undefined
|
|
66
100
|
) {
|
|
67
101
|
this.operator = operator;
|
|
68
102
|
this.value = value;
|
|
69
|
-
this.
|
|
103
|
+
this.property = property;
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
matchesTraitValue(traitValue: any) {
|
|
@@ -79,17 +113,52 @@ export class SegmentConditionModel {
|
|
|
79
113
|
);
|
|
80
114
|
},
|
|
81
115
|
evaluateRegex: (traitValue: any) => {
|
|
82
|
-
|
|
116
|
+
try {
|
|
117
|
+
if (!this.value) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const regex = new RegExp(this.value?.toString());
|
|
121
|
+
return !!traitValue?.toString().match(regex);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
83
125
|
},
|
|
84
126
|
evaluateModulo: (traitValue: any) => {
|
|
85
|
-
|
|
127
|
+
const parsedTraitValue = parseFloat(traitValue);
|
|
128
|
+
if (isNaN(parsedTraitValue) || !this.value) {
|
|
86
129
|
return false;
|
|
87
130
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
131
|
+
|
|
132
|
+
const parts = this.value.toString().split('|');
|
|
133
|
+
if (parts.length !== 2) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const divisor = parseFloat(parts[0]);
|
|
138
|
+
const remainder = parseFloat(parts[1]);
|
|
139
|
+
|
|
140
|
+
if (isNaN(divisor) || isNaN(remainder) || divisor === 0) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return parsedTraitValue % divisor === remainder;
|
|
91
145
|
},
|
|
92
|
-
evaluateIn: (traitValue:
|
|
146
|
+
evaluateIn: (traitValue: string[] | string) => {
|
|
147
|
+
if (!traitValue || typeof traitValue === 'boolean') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(this.value)) {
|
|
151
|
+
return this.value.includes(traitValue.toString());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof this.value === 'string') {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(this.value);
|
|
157
|
+
if (Array.isArray(parsed)) {
|
|
158
|
+
return parsed.includes(traitValue.toString());
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
93
162
|
return this.value?.split(',').includes(traitValue.toString());
|
|
94
163
|
}
|
|
95
164
|
};
|
|
@@ -144,4 +213,83 @@ export class SegmentModel {
|
|
|
144
213
|
this.id = id;
|
|
145
214
|
this.name = name;
|
|
146
215
|
}
|
|
216
|
+
|
|
217
|
+
static fromSegmentResult(
|
|
218
|
+
segmentResults: EvaluationResultSegments,
|
|
219
|
+
evaluationContext: EvaluationContext
|
|
220
|
+
): SegmentModel[] {
|
|
221
|
+
const segmentModels: SegmentModel[] = [];
|
|
222
|
+
if (!evaluationContext.segments) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const segmentResult of segmentResults) {
|
|
227
|
+
if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const segmentMetadataId = segmentResult.metadata?.id;
|
|
231
|
+
if (!segmentMetadataId) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const segmentContext = evaluationContext.segments[segmentMetadataId.toString()];
|
|
235
|
+
if (segmentContext) {
|
|
236
|
+
const segment = new SegmentModel(segmentMetadataId, segmentContext.name);
|
|
237
|
+
segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type));
|
|
238
|
+
segment.featureStates = SegmentModel.createFeatureStatesFromOverrides(
|
|
239
|
+
segmentContext.overrides || []
|
|
240
|
+
);
|
|
241
|
+
segmentModels.push(segment);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return segmentModels;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static createFeatureStatesFromOverrides(overrides: Overrides): FeatureStateModel[] {
|
|
249
|
+
if (!overrides) return [];
|
|
250
|
+
return overrides
|
|
251
|
+
.filter(override => {
|
|
252
|
+
const overrideMetadataId = override?.metadata?.id;
|
|
253
|
+
return typeof overrideMetadataId === 'number';
|
|
254
|
+
})
|
|
255
|
+
.map(override => {
|
|
256
|
+
const overrideMetadataId = override.metadata!.id as number;
|
|
257
|
+
const feature = new FeatureModel(
|
|
258
|
+
overrideMetadataId,
|
|
259
|
+
override.name,
|
|
260
|
+
override.variants?.length && override.variants.length > 0
|
|
261
|
+
? CONSTANTS.MULTIVARIATE
|
|
262
|
+
: CONSTANTS.STANDARD
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const featureState = new FeatureStateModel(
|
|
266
|
+
feature,
|
|
267
|
+
override.enabled,
|
|
268
|
+
override.priority || 0
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (override.value !== undefined) {
|
|
272
|
+
featureState.setValue(override.value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (override.variants && override.variants.length > 0) {
|
|
276
|
+
featureState.multivariateFeatureStateValues = this.createMultivariateValues(
|
|
277
|
+
override.variants
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return featureState;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static createMultivariateValues(variants: any[]): MultivariateFeatureStateValueModel[] {
|
|
286
|
+
return variants.map(
|
|
287
|
+
variant =>
|
|
288
|
+
new MultivariateFeatureStateValueModel(
|
|
289
|
+
new MultivariateFeatureOptionModel(variant.value, variant.id as number),
|
|
290
|
+
variant.weight as number,
|
|
291
|
+
variant.id as number
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
147
295
|
}
|
|
@@ -14,7 +14,7 @@ const makeRepeated = (arr: Array<any>, repeats: number) =>
|
|
|
14
14
|
* @param {} iterations=1 num times to include each id in the generated string to hash
|
|
15
15
|
* @returns number number between 0 (inclusive) and 100 (exclusive)
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function getHashedPercentageForObjIds(objectIds: Array<any>, iterations = 1): number {
|
|
18
18
|
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
19
19
|
const hashedValue = md5(toHash);
|
|
20
20
|
const hashedInt = BigInt('0x' + hashedValue);
|
|
@@ -24,7 +24,7 @@ export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations =
|
|
|
24
24
|
/* istanbul ignore next */
|
|
25
25
|
if (value === 100) {
|
|
26
26
|
/* istanbul ignore next */
|
|
27
|
-
return
|
|
27
|
+
return getHashedPercentageForObjIds(objectIds, iterations + 1);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
return value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.3",
|
|
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/cjs/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -51,26 +51,37 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"lint": "prettier --write .",
|
|
53
53
|
"test": "vitest --coverage --run",
|
|
54
|
+
"test:esm-build": "npm run build && ESM_BUILD=true vitest --config vitest.config.esm.ts --run",
|
|
54
55
|
"test:watch": "vitest",
|
|
55
56
|
"test:debug": "vitest --inspect-brk --no-file-parallelism --coverage",
|
|
56
57
|
"prebuild": "rm -rf ./build",
|
|
57
58
|
"build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}'> build/cjs/package.json",
|
|
58
59
|
"deploy": "npm i && npm run build && npm publish",
|
|
59
60
|
"deploy:beta": "npm i && npm run build && npm publish --tag beta",
|
|
60
|
-
"prepare": "husky install"
|
|
61
|
+
"prepare": "husky install",
|
|
62
|
+
"generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json",
|
|
63
|
+
"generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json",
|
|
64
|
+
"generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types"
|
|
61
65
|
},
|
|
62
66
|
"dependencies": {
|
|
67
|
+
"jsonpath": "^1.1.1",
|
|
63
68
|
"pino": "^8.8.0",
|
|
64
69
|
"semver": "^7.3.7",
|
|
65
70
|
"undici-types": "^6.19.8"
|
|
66
71
|
},
|
|
67
72
|
"devDependencies": {
|
|
73
|
+
"@types/jest": "^30.0.0",
|
|
74
|
+
"@types/jsonpath": "^0.2.4",
|
|
68
75
|
"@types/node": "^20.16.10",
|
|
69
76
|
"@types/semver": "^7.3.9",
|
|
70
77
|
"@types/uuid": "^8.3.4",
|
|
71
78
|
"@vitest/coverage-v8": "^2.1.2",
|
|
72
79
|
"esbuild": "^0.25.0",
|
|
73
80
|
"husky": "^7.0.4",
|
|
81
|
+
"install": "^0.13.0",
|
|
82
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
83
|
+
"jsonc-parser": "^3.3.1",
|
|
84
|
+
"npm": "^11.6.1",
|
|
74
85
|
"prettier": "^2.2.1",
|
|
75
86
|
"typescript": "^4.9.5",
|
|
76
87
|
"undici": "^6.19.8",
|
package/sdk/index.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { Dispatcher } from 'undici-types';
|
|
2
|
-
|
|
3
|
-
getEnvironmentFeatureStates,
|
|
4
|
-
getIdentityFeatureStates
|
|
5
|
-
} from '../flagsmith-engine/index.js';
|
|
6
|
-
import { EnvironmentModel } from '../flagsmith-engine/index.js';
|
|
2
|
+
|
|
7
3
|
import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js';
|
|
8
|
-
import { IdentityModel } from '../flagsmith-engine/index.js';
|
|
9
|
-
import { TraitModel } from '../flagsmith-engine/index.js';
|
|
10
4
|
|
|
11
5
|
import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js';
|
|
12
6
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
13
|
-
import { FlagsmithAPIError } from './errors.js';
|
|
7
|
+
import { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
|
|
14
8
|
|
|
15
9
|
import { DefaultFlag, Flags } from './models.js';
|
|
16
10
|
import { EnvironmentDataPollingManager } from './polling_manager.js';
|
|
17
11
|
import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
|
|
18
|
-
import {
|
|
19
|
-
|
|
12
|
+
import {
|
|
13
|
+
SegmentModel,
|
|
14
|
+
EnvironmentModel,
|
|
15
|
+
IdentityModel,
|
|
16
|
+
TraitModel,
|
|
17
|
+
getEvaluationResult
|
|
18
|
+
} from '../flagsmith-engine/index.js';
|
|
20
19
|
import {
|
|
21
20
|
Fetch,
|
|
22
21
|
FlagsmithCache,
|
|
@@ -25,6 +24,8 @@ import {
|
|
|
25
24
|
TraitConfig
|
|
26
25
|
} from './types.js';
|
|
27
26
|
import { pino, Logger } from 'pino';
|
|
27
|
+
import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js';
|
|
28
|
+
import { EvaluationContextWithMetadata } from '../flagsmith-engine/evaluation/models.js';
|
|
28
29
|
|
|
29
30
|
export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js';
|
|
30
31
|
export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
|
|
@@ -278,7 +279,13 @@ export class Flagsmith {
|
|
|
278
279
|
}))
|
|
279
280
|
);
|
|
280
281
|
|
|
281
|
-
|
|
282
|
+
const context = getEvaluationContext(environment, identityModel);
|
|
283
|
+
if (!context) {
|
|
284
|
+
throw new FlagsmithClientError('Local evaluation required to obtain identity segments');
|
|
285
|
+
}
|
|
286
|
+
const evaluationResult = getEvaluationResult(context as EvaluationContextWithMetadata);
|
|
287
|
+
|
|
288
|
+
return SegmentModel.fromSegmentResult(evaluationResult.segments, context);
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
private async fetchEnvironment(): Promise<EnvironmentModel> {
|
|
@@ -443,14 +450,17 @@ export class Flagsmith {
|
|
|
443
450
|
|
|
444
451
|
private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
|
|
445
452
|
const environment = await this.getEnvironment();
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
453
|
+
const context = getEvaluationContext(environment, undefined, undefined, true);
|
|
454
|
+
if (!context) {
|
|
455
|
+
throw new FlagsmithClientError('Unable to get flags. No environment present.');
|
|
456
|
+
}
|
|
457
|
+
const evaluationResult = getEvaluationResult(context as EvaluationContextWithMetadata);
|
|
458
|
+
const flags = Flags.fromEvaluationResult(evaluationResult);
|
|
459
|
+
|
|
451
460
|
if (!!this.cache) {
|
|
452
461
|
await this.cache.set('flags', flags);
|
|
453
462
|
}
|
|
463
|
+
|
|
454
464
|
return flags;
|
|
455
465
|
}
|
|
456
466
|
|
|
@@ -468,14 +478,17 @@ export class Flagsmith {
|
|
|
468
478
|
}))
|
|
469
479
|
);
|
|
470
480
|
|
|
471
|
-
const
|
|
481
|
+
const context = getEvaluationContext(environment, identityModel);
|
|
482
|
+
if (!context) {
|
|
483
|
+
throw new FlagsmithClientError('Unable to get flags. No environment present.');
|
|
484
|
+
}
|
|
485
|
+
const evaluationResult = getEvaluationResult(context as EvaluationContextWithMetadata);
|
|
472
486
|
|
|
473
|
-
const flags = Flags.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
});
|
|
487
|
+
const flags = Flags.fromEvaluationResult(
|
|
488
|
+
evaluationResult,
|
|
489
|
+
this.defaultFlagHandler,
|
|
490
|
+
this.analyticsProcessor
|
|
491
|
+
);
|
|
479
492
|
|
|
480
493
|
if (!!this.cache) {
|
|
481
494
|
await this.cache.set(`flags-${identifier}`, flags);
|
package/sdk/models.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SDKFeatureMetadata,
|
|
3
|
+
FlagResultWithMetadata,
|
|
4
|
+
EvaluationResultWithMetadata
|
|
5
|
+
} from '../flagsmith-engine/evaluation/models.js';
|
|
1
6
|
import { FeatureStateModel } from '../flagsmith-engine/features/models.js';
|
|
2
7
|
import { AnalyticsProcessor } from './analytics.js';
|
|
3
8
|
|
|
4
|
-
type FlagValue = string | number | boolean | undefined;
|
|
9
|
+
type FlagValue = string | number | boolean | undefined | null;
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}.
|
|
@@ -49,6 +54,10 @@ export class Flag extends BaseFlag {
|
|
|
49
54
|
* The programmatic name for this feature, unique per Flagsmith project.
|
|
50
55
|
*/
|
|
51
56
|
featureName: string;
|
|
57
|
+
/**
|
|
58
|
+
* The reason for this feature, unique per Flagsmith project.
|
|
59
|
+
*/
|
|
60
|
+
reason?: string;
|
|
52
61
|
|
|
53
62
|
constructor(params: {
|
|
54
63
|
value: FlagValue;
|
|
@@ -56,10 +65,12 @@ export class Flag extends BaseFlag {
|
|
|
56
65
|
isDefault?: boolean;
|
|
57
66
|
featureId: number;
|
|
58
67
|
featureName: string;
|
|
68
|
+
reason?: string;
|
|
59
69
|
}) {
|
|
60
70
|
super(params.value, params.enabled, !!params.isDefault);
|
|
61
71
|
this.featureId = params.featureId;
|
|
62
72
|
this.featureName = params.featureName;
|
|
73
|
+
this.reason = params.reason;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
static fromFeatureStateModel(
|
|
@@ -79,7 +90,8 @@ export class Flag extends BaseFlag {
|
|
|
79
90
|
enabled: flagData['enabled'],
|
|
80
91
|
value: flagData['feature_state_value'] ?? flagData['value'],
|
|
81
92
|
featureId: flagData['feature']['id'],
|
|
82
|
-
featureName: flagData['feature']['name']
|
|
93
|
+
featureName: flagData['feature']['name'],
|
|
94
|
+
reason: flagData['feature']['reason']
|
|
83
95
|
});
|
|
84
96
|
}
|
|
85
97
|
}
|
|
@@ -99,6 +111,36 @@ export class Flags {
|
|
|
99
111
|
this.analyticsProcessor = data.analyticsProcessor;
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
static fromEvaluationResult(
|
|
115
|
+
evaluationResult: EvaluationResultWithMetadata,
|
|
116
|
+
defaultFlagHandler?: (v: string) => DefaultFlag,
|
|
117
|
+
analyticsProcessor?: AnalyticsProcessor
|
|
118
|
+
): Flags {
|
|
119
|
+
const flags: { [key: string]: Flag } = {};
|
|
120
|
+
for (const flag of Object.values(evaluationResult.flags)) {
|
|
121
|
+
const flagMetadataId = flag.metadata?.id;
|
|
122
|
+
if (!flagMetadataId) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`FlagResult metadata.id is missing for feature "${flag.name}". ` +
|
|
125
|
+
`This indicates a bug in the SDK, please report it.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
flags[flag.name] = new Flag({
|
|
130
|
+
enabled: flag.enabled,
|
|
131
|
+
value: flag.value ?? null,
|
|
132
|
+
featureId: flagMetadataId,
|
|
133
|
+
featureName: flag.name,
|
|
134
|
+
reason: flag.reason
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return new Flags({
|
|
138
|
+
flags: flags,
|
|
139
|
+
defaultFlagHandler: defaultFlagHandler,
|
|
140
|
+
analyticsProcessor: analyticsProcessor
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
static fromFeatureStateModels(data: {
|
|
103
145
|
featureStates: FeatureStateModel[];
|
|
104
146
|
analyticsProcessor?: AnalyticsProcessor;
|
|
@@ -1,46 +1,51 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { getEvaluationResult } from '../../../flagsmith-engine/index.js';
|
|
5
|
+
import { Flags } from '../../../sdk/models.js';
|
|
6
|
+
import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js';
|
|
7
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
8
|
+
import {
|
|
9
|
+
EvaluationContextWithMetadata,
|
|
10
|
+
EvaluationResult
|
|
11
|
+
} from '../../../flagsmith-engine/evaluation/models.js';
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const identity = buildIdentityModel(test_case['identity']);
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const TEST_DATA_DIR = path.join(__dirname, '../engine-tests/engine-test-data/test_cases');
|
|
16
|
+
interface TestCase {
|
|
17
|
+
context: EvaluationContext;
|
|
18
|
+
result: EvaluationResult;
|
|
19
|
+
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
return test_data;
|
|
21
|
+
function getTestFiles(): string[] {
|
|
22
|
+
const files = fs.readdirSync(TEST_DATA_DIR);
|
|
23
|
+
return files
|
|
24
|
+
.filter(f => f.endsWith('.json') || f.endsWith('.jsonc'))
|
|
25
|
+
.map(f => path.join(TEST_DATA_DIR, f));
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const sortedEngineFlags = engine_response.sort((a, b) =>
|
|
31
|
-
a.feature.name > b.feature.name ? 1 : -1
|
|
32
|
-
);
|
|
33
|
-
const sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) =>
|
|
34
|
-
a.feature.name > b.feature.name ? 1 : -1
|
|
35
|
-
);
|
|
28
|
+
function loadTestFile(filePath: string): TestCase {
|
|
29
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
30
|
+
return parseJsonc(content);
|
|
31
|
+
}
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
describe('Engine Integration Tests', () => {
|
|
34
|
+
const testFiles = getTestFiles();
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sortedAPIFlags[i]['feature_state_value']
|
|
42
|
-
);
|
|
43
|
-
expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i]['enabled']);
|
|
44
|
-
}
|
|
36
|
+
if (testFiles.length === 0) {
|
|
37
|
+
throw new Error(`No test files found in ${TEST_DATA_DIR}`);
|
|
45
38
|
}
|
|
39
|
+
|
|
40
|
+
testFiles.forEach(filePath => {
|
|
41
|
+
const testName = path.basename(filePath, path.extname(filePath));
|
|
42
|
+
|
|
43
|
+
test(testName, () => {
|
|
44
|
+
const testCase = loadTestFile(filePath);
|
|
45
|
+
const engine_response = getEvaluationResult(
|
|
46
|
+
testCase.context as EvaluationContextWithMetadata
|
|
47
|
+
);
|
|
48
|
+
expect(engine_response).toStrictEqual(testCase.result);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
46
51
|
});
|