flagsmith-nodejs 4.0.0-beta.1 → 5.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 +4 -10
- package/build/cjs/flagsmith-engine/features/models.js +4 -4
- package/build/cjs/flagsmith-engine/identities/models.js +2 -2
- package/build/cjs/flagsmith-engine/segments/models.js +6 -9
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +5 -33
- package/build/cjs/index.d.ts +2 -1
- package/build/cjs/index.js +19 -21
- package/build/cjs/sdk/index.js +0 -4
- package/build/cjs/sdk/offline_handlers.js +1 -24
- package/build/cjs/sdk/types.d.ts +14 -4
- package/build/esm/flagsmith-engine/features/models.js +1 -1
- package/build/esm/flagsmith-engine/identities/models.js +1 -1
- package/build/esm/flagsmith-engine/segments/models.js +1 -1
- package/build/esm/flagsmith-engine/utils/hashing/index.js +4 -29
- package/build/esm/index.d.ts +2 -1
- package/build/esm/index.js +2 -3
- package/build/esm/sdk/index.js +0 -4
- package/build/esm/sdk/types.d.ts +14 -4
- package/flagsmith-engine/features/models.ts +1 -1
- package/flagsmith-engine/identities/models.ts +1 -1
- package/flagsmith-engine/segments/models.ts +1 -1
- package/flagsmith-engine/utils/hashing/index.ts +5 -29
- package/index.ts +6 -5
- package/package.json +2 -9
- package/sdk/index.ts +0 -11
- package/sdk/types.ts +17 -4
- package/tests/engine/e2e/engine.test.ts +3 -6
- package/tests/engine/unit/utils/utils.test.ts +1 -1
- package/tests/sdk/flagsmith-cache.test.ts +4 -13
- package/tests/sdk/offline-handlers.test.ts +1 -1
- package/tests/sdk/utils.ts +2 -7
- package/tsconfig.json +1 -1
|
@@ -14,20 +14,14 @@ jobs:
|
|
|
14
14
|
build-and-test:
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
run: |
|
|
21
|
-
# If your submodules are configured to use SSH instead of HTTPS please uncomment the following line
|
|
22
|
-
git config --global url."https://github.com/".insteadOf "git@github.com:"
|
|
23
|
-
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
|
24
|
-
git submodule sync --recursive
|
|
25
|
-
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
submodules: true
|
|
26
20
|
- uses: actions/setup-node@v4
|
|
27
21
|
with:
|
|
28
22
|
node-version: "18.x"
|
|
29
23
|
- name: cache node modules
|
|
30
|
-
uses: actions/cache@
|
|
24
|
+
uses: actions/cache@v4
|
|
31
25
|
with:
|
|
32
26
|
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
|
|
33
27
|
key: npm-${{ hashFiles('package-lock.json') }}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FeatureSegment = exports.FeatureStateModel = exports.MultivariateFeatureStateValueModel = exports.MultivariateFeatureOptionModel = exports.FeatureModel = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const index_js_1 = require("../utils/hashing/index.js");
|
|
6
6
|
class FeatureModel {
|
|
7
7
|
id;
|
|
@@ -30,7 +30,7 @@ class MultivariateFeatureStateValueModel {
|
|
|
30
30
|
multivariateFeatureOption;
|
|
31
31
|
percentageAllocation;
|
|
32
32
|
id;
|
|
33
|
-
mvFsValueUuid = (0,
|
|
33
|
+
mvFsValueUuid = (0, node_crypto_1.randomUUID)();
|
|
34
34
|
constructor(multivariate_feature_option, percentage_allocation, id, mvFsValueUuid) {
|
|
35
35
|
this.id = id;
|
|
36
36
|
this.percentageAllocation = percentage_allocation;
|
|
@@ -43,11 +43,11 @@ class FeatureStateModel {
|
|
|
43
43
|
feature;
|
|
44
44
|
enabled;
|
|
45
45
|
djangoID;
|
|
46
|
-
featurestateUUID = (0,
|
|
46
|
+
featurestateUUID = (0, node_crypto_1.randomUUID)();
|
|
47
47
|
featureSegment;
|
|
48
48
|
value;
|
|
49
49
|
multivariateFeatureStateValues = [];
|
|
50
|
-
constructor(feature, enabled, djangoID, value, featurestateUuid = (0,
|
|
50
|
+
constructor(feature, enabled, djangoID, value, featurestateUuid = (0, node_crypto_1.randomUUID)()) {
|
|
51
51
|
this.feature = feature;
|
|
52
52
|
this.enabled = enabled;
|
|
53
53
|
this.djangoID = djangoID;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.IdentityModel = void 0;
|
|
4
4
|
const collections_js_1 = require("../utils/collections.js");
|
|
5
|
-
const
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
class IdentityModel {
|
|
7
7
|
identifier;
|
|
8
8
|
environmentApiKey;
|
|
@@ -12,7 +12,7 @@ class IdentityModel {
|
|
|
12
12
|
identityUuid;
|
|
13
13
|
djangoID;
|
|
14
14
|
constructor(created_date, identityTraits, identityFeatures, environmentApiKey, identifier, identityUuid, djangoID) {
|
|
15
|
-
this.identityUuid = identityUuid ||
|
|
15
|
+
this.identityUuid = identityUuid || (0, node_crypto_1.randomUUID)();
|
|
16
16
|
this.createdDate = Date.parse(created_date) || Date.now();
|
|
17
17
|
this.identityTraits = identityTraits;
|
|
18
18
|
this.identityFeatures = new collections_js_1.IdentityFeaturesList(...identityFeatures);
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.SegmentModel = exports.SegmentRuleModel = exports.SegmentConditionModel = exports.getMatchingFunctions = exports.semverMatchingFunction = exports.matchingFunctions = exports.any = exports.all = void 0;
|
|
7
|
-
const
|
|
4
|
+
const semver = require("semver");
|
|
8
5
|
const index_js_1 = require("../utils/index.js");
|
|
9
6
|
const constants_js_1 = require("./constants.js");
|
|
10
7
|
const util_js_1 = require("./util.js");
|
|
@@ -23,11 +20,11 @@ exports.matchingFunctions = {
|
|
|
23
20
|
};
|
|
24
21
|
exports.semverMatchingFunction = {
|
|
25
22
|
...exports.matchingFunctions,
|
|
26
|
-
[constants_js_1.CONDITION_OPERATORS.EQUAL]: (thisValue, otherValue) =>
|
|
27
|
-
[constants_js_1.CONDITION_OPERATORS.GREATER_THAN]: (thisValue, otherValue) =>
|
|
28
|
-
[constants_js_1.CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue, otherValue) =>
|
|
29
|
-
[constants_js_1.CONDITION_OPERATORS.LESS_THAN]: (thisValue, otherValue) =>
|
|
30
|
-
[constants_js_1.CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue, otherValue) =>
|
|
23
|
+
[constants_js_1.CONDITION_OPERATORS.EQUAL]: (thisValue, otherValue) => semver.eq(thisValue, otherValue),
|
|
24
|
+
[constants_js_1.CONDITION_OPERATORS.GREATER_THAN]: (thisValue, otherValue) => semver.gt(otherValue, thisValue),
|
|
25
|
+
[constants_js_1.CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue, otherValue) => semver.gte(otherValue, thisValue),
|
|
26
|
+
[constants_js_1.CONDITION_OPERATORS.LESS_THAN]: (thisValue, otherValue) => semver.gt(thisValue, otherValue),
|
|
27
|
+
[constants_js_1.CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue, otherValue) => semver.gte(thisValue, otherValue),
|
|
31
28
|
};
|
|
32
29
|
const getMatchingFunctions = (semver) => (semver ? exports.semverMatchingFunction : exports.matchingFunctions);
|
|
33
30
|
exports.getMatchingFunctions = getMatchingFunctions;
|
|
@@ -1,38 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.getHashedPercentateForObjIds = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const md5 = (data) => (0, node_crypto_1.createHash)('md5').update(data).digest('hex');
|
|
9
6
|
const makeRepeated = (arr, repeats) => Array.from({ length: repeats }, () => arr).flat();
|
|
10
7
|
// https://stackoverflow.com/questions/12532871/how-to-convert-a-very-large-hex-number-to-decimal-in-javascript
|
|
11
|
-
function h2d(s) {
|
|
12
|
-
function add(x, y) {
|
|
13
|
-
var c = 0, r = [];
|
|
14
|
-
var x = x.split('').map(Number);
|
|
15
|
-
var y = y.split('').map(Number);
|
|
16
|
-
while (x.length || y.length) {
|
|
17
|
-
var s = (x.pop() || 0) + (y.pop() || 0) + c;
|
|
18
|
-
r.unshift(s < 10 ? s : s - 10);
|
|
19
|
-
c = s < 10 ? 0 : 1;
|
|
20
|
-
}
|
|
21
|
-
if (c)
|
|
22
|
-
r.unshift(c);
|
|
23
|
-
return r.join('');
|
|
24
|
-
}
|
|
25
|
-
var dec = '0';
|
|
26
|
-
s.split('').forEach(function (chr) {
|
|
27
|
-
var n = parseInt(chr, 16);
|
|
28
|
-
for (var t = 8; t; t >>= 1) {
|
|
29
|
-
dec = add(dec, dec);
|
|
30
|
-
if (n & t)
|
|
31
|
-
dec = add(dec, '1');
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return dec;
|
|
35
|
-
}
|
|
36
8
|
/**
|
|
37
9
|
* Given a list of object ids, get a floating point number between 0 and 1 based on
|
|
38
10
|
* the hash of those ids. This should give the same value every time for any list of ids.
|
|
@@ -43,9 +15,9 @@ function h2d(s) {
|
|
|
43
15
|
*/
|
|
44
16
|
function getHashedPercentateForObjIds(objectIds, iterations = 1) {
|
|
45
17
|
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
46
|
-
const hashedValue = (
|
|
47
|
-
const hashedInt = (
|
|
48
|
-
const value = (
|
|
18
|
+
const hashedValue = md5(toHash);
|
|
19
|
+
const hashedInt = BigInt('0x' + hashedValue);
|
|
20
|
+
const value = (Number((hashedInt % 9999n)) / 9998.0) * 100;
|
|
49
21
|
// we ignore this for it's nearly impossible use case to catch
|
|
50
22
|
/* istanbul ignore next */
|
|
51
23
|
if (value === 100) {
|
package/build/cjs/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, DefaultFlag, Flags,
|
|
1
|
+
export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, DefaultFlag, Flags, Flagsmith, } from './sdk/index.js';
|
|
2
|
+
export { BaseOfflineHandler, LocalFileHandler, } from './sdk/offline_handlers.js';
|
|
2
3
|
export { FlagsmithConfig } from './sdk/types.js';
|
|
3
4
|
export { EnvironmentModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
|
package/build/cjs/index.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.OrganisationModel = exports.SegmentModel = exports.TraitModel = exports.IdentityModel = exports.FeatureStateModel = exports.EnvironmentModel = exports.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(exports, "
|
|
10
|
-
Object.defineProperty(exports, "
|
|
11
|
-
Object.defineProperty(exports, "
|
|
12
|
-
Object.defineProperty(exports, "
|
|
13
|
-
Object.defineProperty(exports, "
|
|
14
|
-
Object.defineProperty(exports, "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Object.defineProperty(exports, "
|
|
18
|
-
|
|
19
|
-
Object.defineProperty(exports, "
|
|
20
|
-
Object.defineProperty(exports, "
|
|
21
|
-
Object.defineProperty(exports, "
|
|
22
|
-
Object.defineProperty(exports, "
|
|
23
|
-
|
|
3
|
+
exports.OrganisationModel = exports.SegmentModel = exports.TraitModel = exports.IdentityModel = exports.FeatureStateModel = exports.EnvironmentModel = exports.LocalFileHandler = exports.BaseOfflineHandler = exports.Flagsmith = exports.Flags = exports.DefaultFlag = exports.EnvironmentDataPollingManager = exports.FlagsmithClientError = exports.FlagsmithAPIError = exports.AnalyticsProcessor = void 0;
|
|
4
|
+
var index_js_1 = require("./sdk/index.js");
|
|
5
|
+
Object.defineProperty(exports, "AnalyticsProcessor", { enumerable: true, get: function () { return index_js_1.AnalyticsProcessor; } });
|
|
6
|
+
Object.defineProperty(exports, "FlagsmithAPIError", { enumerable: true, get: function () { return index_js_1.FlagsmithAPIError; } });
|
|
7
|
+
Object.defineProperty(exports, "FlagsmithClientError", { enumerable: true, get: function () { return index_js_1.FlagsmithClientError; } });
|
|
8
|
+
Object.defineProperty(exports, "EnvironmentDataPollingManager", { enumerable: true, get: function () { return index_js_1.EnvironmentDataPollingManager; } });
|
|
9
|
+
Object.defineProperty(exports, "DefaultFlag", { enumerable: true, get: function () { return index_js_1.DefaultFlag; } });
|
|
10
|
+
Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return index_js_1.Flags; } });
|
|
11
|
+
Object.defineProperty(exports, "Flagsmith", { enumerable: true, get: function () { return index_js_1.Flagsmith; } });
|
|
12
|
+
var offline_handlers_js_1 = require("./sdk/offline_handlers.js");
|
|
13
|
+
Object.defineProperty(exports, "BaseOfflineHandler", { enumerable: true, get: function () { return offline_handlers_js_1.BaseOfflineHandler; } });
|
|
14
|
+
Object.defineProperty(exports, "LocalFileHandler", { enumerable: true, get: function () { return offline_handlers_js_1.LocalFileHandler; } });
|
|
15
|
+
var index_js_2 = require("./flagsmith-engine/index.js");
|
|
16
|
+
Object.defineProperty(exports, "EnvironmentModel", { enumerable: true, get: function () { return index_js_2.EnvironmentModel; } });
|
|
17
|
+
Object.defineProperty(exports, "FeatureStateModel", { enumerable: true, get: function () { return index_js_2.FeatureStateModel; } });
|
|
18
|
+
Object.defineProperty(exports, "IdentityModel", { enumerable: true, get: function () { return index_js_2.IdentityModel; } });
|
|
19
|
+
Object.defineProperty(exports, "TraitModel", { enumerable: true, get: function () { return index_js_2.TraitModel; } });
|
|
20
|
+
Object.defineProperty(exports, "SegmentModel", { enumerable: true, get: function () { return index_js_2.SegmentModel; } });
|
|
21
|
+
Object.defineProperty(exports, "OrganisationModel", { enumerable: true, get: function () { return index_js_2.OrganisationModel; } });
|
package/build/cjs/sdk/index.js
CHANGED
|
@@ -117,10 +117,6 @@ class Flagsmith {
|
|
|
117
117
|
this.environment = this.offlineHandler.getEnvironment();
|
|
118
118
|
}
|
|
119
119
|
if (!!data.cache) {
|
|
120
|
-
const missingMethods = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
|
|
121
|
-
if (missingMethods.length > 0) {
|
|
122
|
-
throw new Error(`Please implement the following methods in your cache: ${missingMethods.join(', ')}`);
|
|
123
|
-
}
|
|
124
120
|
this.cache = data.cache;
|
|
125
121
|
}
|
|
126
122
|
if (!this.offlineMode) {
|
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
3
|
exports.LocalFileHandler = exports.BaseOfflineHandler = void 0;
|
|
27
|
-
const fs =
|
|
4
|
+
const fs = require("fs");
|
|
28
5
|
const util_js_1 = require("../flagsmith-engine/environments/util.js");
|
|
29
6
|
class BaseOfflineHandler {
|
|
30
7
|
getEnvironment() {
|
package/build/cjs/sdk/types.d.ts
CHANGED
|
@@ -4,11 +4,21 @@ import { Dispatcher } from 'undici-types';
|
|
|
4
4
|
import { Logger } from 'pino';
|
|
5
5
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
6
6
|
export type IFlagsmithValue<T = string | number | boolean | null> = T;
|
|
7
|
+
/**
|
|
8
|
+
* Stores and retrieves {@link Flags} from a cache.
|
|
9
|
+
*/
|
|
7
10
|
export interface FlagsmithCache {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Retrieve the cached {@link Flags} for the given environment or identity, or `undefined` if no cached value exists.
|
|
13
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
14
|
+
*/
|
|
15
|
+
get(key: string): Promise<Flags | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Persist an environment or identity's {@link Flags} in the cache.
|
|
18
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
19
|
+
* @param value The {@link Flags} to be stored in the cache.
|
|
20
|
+
*/
|
|
21
|
+
set(key: string, value: Flags): Promise<void>;
|
|
12
22
|
}
|
|
13
23
|
export type Fetch = typeof fetch;
|
|
14
24
|
export interface FlagsmithConfig {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import semver from 'semver';
|
|
1
|
+
import * as semver from 'semver';
|
|
2
2
|
import { getCastingFunction as getCastingFunction } from '../utils/index.js';
|
|
3
3
|
import { ALL_RULE, ANY_RULE, NONE_RULE, NOT_CONTAINS, REGEX, MODULO, IN, CONDITION_OPERATORS } from './constants.js';
|
|
4
4
|
import { isSemver } from './util.js';
|
|
@@ -1,32 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const md5 = (data) => createHash('md5').update(data).digest('hex');
|
|
3
3
|
const makeRepeated = (arr, repeats) => Array.from({ length: repeats }, () => arr).flat();
|
|
4
4
|
// https://stackoverflow.com/questions/12532871/how-to-convert-a-very-large-hex-number-to-decimal-in-javascript
|
|
5
|
-
function h2d(s) {
|
|
6
|
-
function add(x, y) {
|
|
7
|
-
var c = 0, r = [];
|
|
8
|
-
var x = x.split('').map(Number);
|
|
9
|
-
var y = y.split('').map(Number);
|
|
10
|
-
while (x.length || y.length) {
|
|
11
|
-
var s = (x.pop() || 0) + (y.pop() || 0) + c;
|
|
12
|
-
r.unshift(s < 10 ? s : s - 10);
|
|
13
|
-
c = s < 10 ? 0 : 1;
|
|
14
|
-
}
|
|
15
|
-
if (c)
|
|
16
|
-
r.unshift(c);
|
|
17
|
-
return r.join('');
|
|
18
|
-
}
|
|
19
|
-
var dec = '0';
|
|
20
|
-
s.split('').forEach(function (chr) {
|
|
21
|
-
var n = parseInt(chr, 16);
|
|
22
|
-
for (var t = 8; t; t >>= 1) {
|
|
23
|
-
dec = add(dec, dec);
|
|
24
|
-
if (n & t)
|
|
25
|
-
dec = add(dec, '1');
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
return dec;
|
|
29
|
-
}
|
|
30
5
|
/**
|
|
31
6
|
* Given a list of object ids, get a floating point number between 0 and 1 based on
|
|
32
7
|
* the hash of those ids. This should give the same value every time for any list of ids.
|
|
@@ -38,8 +13,8 @@ function h2d(s) {
|
|
|
38
13
|
export function getHashedPercentateForObjIds(objectIds, iterations = 1) {
|
|
39
14
|
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
40
15
|
const hashedValue = md5(toHash);
|
|
41
|
-
const hashedInt =
|
|
42
|
-
const value = (
|
|
16
|
+
const hashedInt = BigInt('0x' + hashedValue);
|
|
17
|
+
const value = (Number((hashedInt % 9999n)) / 9998.0) * 100;
|
|
43
18
|
// we ignore this for it's nearly impossible use case to catch
|
|
44
19
|
/* istanbul ignore next */
|
|
45
20
|
if (value === 100) {
|
package/build/esm/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, DefaultFlag, Flags,
|
|
1
|
+
export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, DefaultFlag, Flags, Flagsmith, } from './sdk/index.js';
|
|
2
|
+
export { BaseOfflineHandler, LocalFileHandler, } from './sdk/offline_handlers.js';
|
|
2
3
|
export { FlagsmithConfig } from './sdk/types.js';
|
|
3
4
|
export { EnvironmentModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
|
package/build/esm/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, DefaultFlag, Flags, Flagsmith, } from './sdk/index.js';
|
|
2
|
+
export { BaseOfflineHandler, LocalFileHandler, } from './sdk/offline_handlers.js';
|
|
3
3
|
export { EnvironmentModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
|
|
4
|
-
module.exports = Flagsmith;
|
package/build/esm/sdk/index.js
CHANGED
|
@@ -108,10 +108,6 @@ export class Flagsmith {
|
|
|
108
108
|
this.environment = this.offlineHandler.getEnvironment();
|
|
109
109
|
}
|
|
110
110
|
if (!!data.cache) {
|
|
111
|
-
const missingMethods = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
|
|
112
|
-
if (missingMethods.length > 0) {
|
|
113
|
-
throw new Error(`Please implement the following methods in your cache: ${missingMethods.join(', ')}`);
|
|
114
|
-
}
|
|
115
111
|
this.cache = data.cache;
|
|
116
112
|
}
|
|
117
113
|
if (!this.offlineMode) {
|
package/build/esm/sdk/types.d.ts
CHANGED
|
@@ -4,11 +4,21 @@ import { Dispatcher } from 'undici-types';
|
|
|
4
4
|
import { Logger } from 'pino';
|
|
5
5
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
6
6
|
export type IFlagsmithValue<T = string | number | boolean | null> = T;
|
|
7
|
+
/**
|
|
8
|
+
* Stores and retrieves {@link Flags} from a cache.
|
|
9
|
+
*/
|
|
7
10
|
export interface FlagsmithCache {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Retrieve the cached {@link Flags} for the given environment or identity, or `undefined` if no cached value exists.
|
|
13
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
14
|
+
*/
|
|
15
|
+
get(key: string): Promise<Flags | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Persist an environment or identity's {@link Flags} in the cache.
|
|
18
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
19
|
+
* @param value The {@link Flags} to be stored in the cache.
|
|
20
|
+
*/
|
|
21
|
+
set(key: string, value: Flags): Promise<void>;
|
|
12
22
|
}
|
|
13
23
|
export type Fetch = typeof fetch;
|
|
14
24
|
export interface FlagsmithConfig {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IdentityFeaturesList } from '../utils/collections.js';
|
|
2
2
|
import { TraitModel } from './traits/models.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { randomUUID as uuidv4 } from 'node:crypto';
|
|
5
5
|
|
|
6
6
|
export class IdentityModel {
|
|
7
7
|
identifier: string;
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {BinaryLike, createHash} from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const md5 = (data: BinaryLike) => createHash('md5').update(data).digest('hex')
|
|
3
4
|
|
|
4
5
|
const makeRepeated = (arr: Array<any>, repeats: number) =>
|
|
5
6
|
Array.from({ length: repeats }, () => arr).flat();
|
|
6
7
|
|
|
7
8
|
// https://stackoverflow.com/questions/12532871/how-to-convert-a-very-large-hex-number-to-decimal-in-javascript
|
|
8
|
-
function h2d(s: any): string {
|
|
9
|
-
function add(x: any, y: any) {
|
|
10
|
-
var c = 0,
|
|
11
|
-
r = [];
|
|
12
|
-
var x = x.split('').map(Number);
|
|
13
|
-
var y = y.split('').map(Number);
|
|
14
|
-
while (x.length || y.length) {
|
|
15
|
-
var s = (x.pop() || 0) + (y.pop() || 0) + c;
|
|
16
|
-
r.unshift(s < 10 ? s : s - 10);
|
|
17
|
-
c = s < 10 ? 0 : 1;
|
|
18
|
-
}
|
|
19
|
-
if (c) r.unshift(c);
|
|
20
|
-
return r.join('');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
var dec = '0';
|
|
24
|
-
s.split('').forEach(function (chr: any) {
|
|
25
|
-
var n = parseInt(chr, 16);
|
|
26
|
-
for (var t = 8; t; t >>= 1) {
|
|
27
|
-
dec = add(dec, dec);
|
|
28
|
-
if (n & t) dec = add(dec, '1');
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
return dec;
|
|
32
|
-
}
|
|
33
9
|
/**
|
|
34
10
|
* Given a list of object ids, get a floating point number between 0 and 1 based on
|
|
35
11
|
* the hash of those ids. This should give the same value every time for any list of ids.
|
|
@@ -41,8 +17,8 @@ function h2d(s: any): string {
|
|
|
41
17
|
export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations = 1): number {
|
|
42
18
|
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
43
19
|
const hashedValue = md5(toHash);
|
|
44
|
-
const hashedInt =
|
|
45
|
-
const value = (
|
|
20
|
+
const hashedInt = BigInt('0x' + hashedValue);
|
|
21
|
+
const value = (Number((hashedInt % 9999n)) / 9998.0) * 100;
|
|
46
22
|
|
|
47
23
|
// we ignore this for it's nearly impossible use case to catch
|
|
48
24
|
/* istanbul ignore next */
|
package/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import Flagsmith from "./sdk/index.js";
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
2
|
AnalyticsProcessor,
|
|
5
3
|
FlagsmithAPIError,
|
|
@@ -8,9 +6,14 @@ export {
|
|
|
8
6
|
FlagsmithCache,
|
|
9
7
|
DefaultFlag,
|
|
10
8
|
Flags,
|
|
11
|
-
|
|
9
|
+
Flagsmith,
|
|
12
10
|
} from './sdk/index.js';
|
|
13
11
|
|
|
12
|
+
export {
|
|
13
|
+
BaseOfflineHandler,
|
|
14
|
+
LocalFileHandler,
|
|
15
|
+
} from './sdk/offline_handlers.js';
|
|
16
|
+
|
|
14
17
|
export {
|
|
15
18
|
FlagsmithConfig
|
|
16
19
|
} from './sdk/types.js'
|
|
@@ -23,5 +26,3 @@ export {
|
|
|
23
26
|
SegmentModel,
|
|
24
27
|
OrganisationModel
|
|
25
28
|
} from './flagsmith-engine/index.js';
|
|
26
|
-
|
|
27
|
-
module.exports = Flagsmith;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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/cjs/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -60,25 +60,18 @@
|
|
|
60
60
|
"prepare": "husky install"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"big-integer": "^1.6.51",
|
|
64
|
-
"md5": "^2.3.0",
|
|
65
63
|
"pino": "^8.8.0",
|
|
66
64
|
"semver": "^7.3.7",
|
|
67
|
-
"undici-types": "^6.19.8"
|
|
68
|
-
"uuid": "^8.3.2"
|
|
65
|
+
"undici-types": "^6.19.8"
|
|
69
66
|
},
|
|
70
67
|
"devDependencies": {
|
|
71
|
-
"@types/jest": "^27.4.1",
|
|
72
|
-
"@types/md5": "^2.3.2",
|
|
73
68
|
"@types/node": "^20.16.10",
|
|
74
69
|
"@types/semver": "^7.3.9",
|
|
75
70
|
"@types/uuid": "^8.3.4",
|
|
76
71
|
"@vitest/coverage-v8": "^2.1.2",
|
|
77
72
|
"esbuild": "^0.14.25",
|
|
78
73
|
"husky": "^7.0.4",
|
|
79
|
-
"jest": "^27.5.1",
|
|
80
74
|
"prettier": "^2.2.1",
|
|
81
|
-
"ts-jest": "^27.1.3",
|
|
82
75
|
"typescript": "^4.9.5",
|
|
83
76
|
"undici": "^6.19.8",
|
|
84
77
|
"vitest": "^2.1.2"
|
package/sdk/index.ts
CHANGED
|
@@ -128,17 +128,6 @@ export class Flagsmith {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
if (!!data.cache) {
|
|
131
|
-
const missingMethods: string[] = ['has', 'get', 'set'].filter(
|
|
132
|
-
method => data.cache && !data.cache[method]
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
if (missingMethods.length > 0) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`Please implement the following methods in your cache: ${missingMethods.join(
|
|
138
|
-
', '
|
|
139
|
-
)}`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
131
|
this.cache = data.cache;
|
|
143
132
|
}
|
|
144
133
|
|
package/sdk/types.ts
CHANGED
|
@@ -5,11 +5,24 @@ import { Logger } from 'pino';
|
|
|
5
5
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
6
6
|
|
|
7
7
|
export type IFlagsmithValue<T = string | number | boolean | null> = T;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stores and retrieves {@link Flags} from a cache.
|
|
12
|
+
*/
|
|
8
13
|
export interface FlagsmithCache {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve the cached {@link Flags} for the given environment or identity, or `undefined` if no cached value exists.
|
|
16
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
17
|
+
*/
|
|
18
|
+
get(key: string): Promise<Flags | undefined>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Persist an environment or identity's {@link Flags} in the cache.
|
|
22
|
+
* @param key An environment ID or identity identifier, which is used as the cache key.
|
|
23
|
+
* @param value The {@link Flags} to be stored in the cache.
|
|
24
|
+
*/
|
|
25
|
+
set(key: string, value: Flags): Promise<void>;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
export type Fetch = typeof fetch
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
1
|
import { getIdentityFeatureStates } from '../../../flagsmith-engine/index.js';
|
|
3
2
|
import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js';
|
|
4
3
|
import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js';
|
|
5
4
|
import { IdentityModel } from '../../../flagsmith-engine/identities/models.js';
|
|
6
5
|
import { buildIdentityModel } from '../../../flagsmith-engine/identities/util.js';
|
|
6
|
+
import * as testData from '../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'
|
|
7
7
|
|
|
8
8
|
function extractTestCases(
|
|
9
|
-
|
|
9
|
+
data: any
|
|
10
10
|
): {
|
|
11
11
|
environment: EnvironmentModel;
|
|
12
12
|
identity: IdentityModel;
|
|
13
13
|
response: any;
|
|
14
14
|
}[] {
|
|
15
|
-
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
16
15
|
const environmentModel = buildEnvironmentModel(data['environment']);
|
|
17
16
|
const test_data = data['identities_and_responses'].map((test_case: any) => {
|
|
18
17
|
const identity = buildIdentityModel(test_case['identity']);
|
|
@@ -27,9 +26,7 @@ function extractTestCases(
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
test('Test Engine', () => {
|
|
30
|
-
const testCases = extractTestCases(
|
|
31
|
-
__dirname + '/../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'
|
|
32
|
-
);
|
|
29
|
+
const testCases = extractTestCases(testData);
|
|
33
30
|
for (const testCase of testCases) {
|
|
34
31
|
const engine_response = getIdentityFeatureStates(testCase.environment, testCase.identity);
|
|
35
32
|
const sortedEngineFlags = engine_response.sort((a, b) =>
|
|
@@ -4,15 +4,6 @@ beforeEach(() => {
|
|
|
4
4
|
vi.clearAllMocks();
|
|
5
5
|
});
|
|
6
6
|
|
|
7
|
-
test('test_wrong_cache_interface_throws_an_error', async () => {
|
|
8
|
-
const cache = {
|
|
9
|
-
set: () => { },
|
|
10
|
-
get: () => { },
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
expect(() => { const flg = flagsmith({ cache }); }).toThrow();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
7
|
test('test_empty_cache_not_read_but_populated', async () => {
|
|
17
8
|
fetch.mockResolvedValue(new Response(flagsJSON));
|
|
18
9
|
|
|
@@ -23,7 +14,7 @@ test('test_empty_cache_not_read_but_populated', async () => {
|
|
|
23
14
|
const allFlags = (await flg.getEnvironmentFlags()).allFlags();
|
|
24
15
|
|
|
25
16
|
expect(set).toBeCalled();
|
|
26
|
-
expect(await cache.
|
|
17
|
+
expect(await cache.get('flags')).toBeTruthy();
|
|
27
18
|
|
|
28
19
|
expect(fetch).toBeCalledTimes(1);
|
|
29
20
|
expect(allFlags[0].enabled).toBe(true);
|
|
@@ -42,7 +33,7 @@ test('test_api_not_called_when_cache_present', async () => {
|
|
|
42
33
|
const allFlags = await (await flg.getEnvironmentFlags()).allFlags();
|
|
43
34
|
|
|
44
35
|
expect(set).toBeCalled();
|
|
45
|
-
expect(await cache.
|
|
36
|
+
expect(await cache.get('flags')).toBeTruthy();
|
|
46
37
|
|
|
47
38
|
expect(fetch).toBeCalledTimes(1);
|
|
48
39
|
expect(allFlags[0].enabled).toBe(true);
|
|
@@ -97,7 +88,7 @@ test('test_cache_used_for_identity_flags', async () => {
|
|
|
97
88
|
const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
98
89
|
|
|
99
90
|
expect(set).toBeCalled();
|
|
100
|
-
expect(await cache.
|
|
91
|
+
expect(await cache.get('flags-identifier')).toBeTruthy();
|
|
101
92
|
|
|
102
93
|
expect(fetch).toBeCalledTimes(1);
|
|
103
94
|
|
|
@@ -124,7 +115,7 @@ test('test_cache_used_for_identity_flags_local_evaluation', async () => {
|
|
|
124
115
|
const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
125
116
|
|
|
126
117
|
expect(set).toBeCalled();
|
|
127
|
-
expect(await cache.
|
|
118
|
+
expect(await cache.get('flags-identifier')).toBeTruthy();
|
|
128
119
|
|
|
129
120
|
expect(fetch).toBeCalledTimes(1);
|
|
130
121
|
|
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import { LocalFileHandler } from '../../sdk/offline_handlers.js';
|
|
3
3
|
import { EnvironmentModel } from '../../flagsmith-engine/index.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import * as offlineEnvironment from "./data/offline-environment.json";
|
|
6
6
|
|
|
7
7
|
vi.mock('fs')
|
|
8
8
|
|
package/tests/sdk/utils.ts
CHANGED
|
@@ -10,17 +10,12 @@ const DATA_DIR = __dirname + '/data/';
|
|
|
10
10
|
export class TestCache implements FlagsmithCache {
|
|
11
11
|
cache: Record<string, Flags> = {};
|
|
12
12
|
|
|
13
|
-
async get(name: string): Promise<Flags> {
|
|
13
|
+
async get(name: string): Promise<Flags | undefined> {
|
|
14
14
|
return this.cache[name];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async
|
|
18
|
-
return !!this.cache[name];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async set(name: string, value: Flags, ttl: number|string) {
|
|
17
|
+
async set(name: string, value: Flags) {
|
|
22
18
|
this.cache[name] = value;
|
|
23
|
-
return true
|
|
24
19
|
}
|
|
25
20
|
}
|
|
26
21
|
|
package/tsconfig.json
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
/* Modules */
|
|
12
12
|
"module": "ESNext",
|
|
13
13
|
"moduleResolution": "Node16",
|
|
14
|
-
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
|
15
14
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
16
15
|
"declaration": true,
|
|
17
16
|
"strict": true, /* Enable all strict type-checking options. */
|
|
18
17
|
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
|
19
18
|
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
20
|
"types": [
|
|
21
21
|
"vitest/globals"
|
|
22
22
|
]
|