flagsmith-nodejs 3.1.0 → 3.2.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/build/sdk/index.d.ts +17 -9
- package/build/sdk/index.js +99 -43
- package/build/sdk/offline_handlers.d.ts +9 -0
- package/build/sdk/offline_handlers.js +66 -0
- package/build/sdk/types.d.ts +4 -1
- package/package.json +1 -1
- package/sdk/index.ts +105 -47
- package/sdk/offline_handlers.ts +22 -0
- package/sdk/types.ts +4 -1
- package/tests/sdk/data/offline-environment.json +93 -0
- package/tests/sdk/flagsmith.test.ts +114 -6
- package/tests/sdk/offline-handlers.test.ts +33 -0
- package/tests/sdk/utils.ts +2 -2
package/build/sdk/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { RequestInit } from
|
|
1
|
+
import { RequestInit } from 'node-fetch';
|
|
2
2
|
import { EnvironmentModel } from '../flagsmith-engine/environments/models';
|
|
3
|
+
import { BaseOfflineHandler } from './offline_handlers';
|
|
3
4
|
import { DefaultFlag, Flags } from './models';
|
|
4
5
|
import { EnvironmentDataPollingManager } from './polling_manager';
|
|
5
6
|
import { SegmentModel } from '../flagsmith-engine/segments/models';
|
|
@@ -11,11 +12,10 @@ export { EnvironmentDataPollingManager } from './polling_manager';
|
|
|
11
12
|
export { FlagsmithCache, FlagsmithConfig } from './types';
|
|
12
13
|
export declare class Flagsmith {
|
|
13
14
|
environmentKey?: string;
|
|
14
|
-
apiUrl
|
|
15
|
+
apiUrl?: string;
|
|
15
16
|
customHeaders?: {
|
|
16
17
|
[key: string]: any;
|
|
17
18
|
};
|
|
18
|
-
requestTimeoutSeconds?: number;
|
|
19
19
|
agent: RequestInit['agent'];
|
|
20
20
|
requestTimeoutMs?: number;
|
|
21
21
|
enableLocalEvaluation?: boolean;
|
|
@@ -23,11 +23,13 @@ export declare class Flagsmith {
|
|
|
23
23
|
retries?: number;
|
|
24
24
|
enableAnalytics: boolean;
|
|
25
25
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
26
|
-
environmentFlagsUrl
|
|
27
|
-
identitiesUrl
|
|
28
|
-
environmentUrl
|
|
26
|
+
environmentFlagsUrl?: string;
|
|
27
|
+
identitiesUrl?: string;
|
|
28
|
+
environmentUrl?: string;
|
|
29
29
|
environmentDataPollingManager?: EnvironmentDataPollingManager;
|
|
30
30
|
environment: EnvironmentModel;
|
|
31
|
+
offlineMode: boolean;
|
|
32
|
+
offlineHandler?: BaseOfflineHandler;
|
|
31
33
|
private cache?;
|
|
32
34
|
private onEnvironmentChange?;
|
|
33
35
|
private analyticsProcessor?;
|
|
@@ -46,6 +48,7 @@ export declare class Flagsmith {
|
|
|
46
48
|
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
47
49
|
*
|
|
48
50
|
* @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
|
|
51
|
+
* Required unless offlineMode is True.
|
|
49
52
|
@param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
|
|
50
53
|
@param data.customHeaders: Additional headers to add to requests made to the
|
|
51
54
|
Flagsmith API
|
|
@@ -59,11 +62,16 @@ export declare class Flagsmith {
|
|
|
59
62
|
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
60
63
|
API to power flag analytics charts
|
|
61
64
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
62
|
-
flags cannot be retrieved from the API or a non
|
|
65
|
+
flags cannot be retrieved from the API or a non-existent feature is
|
|
63
66
|
requested
|
|
64
67
|
@param data.logger: an instance of the pino Logger class to use for logging
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
@param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for
|
|
69
|
+
evaluating flags.
|
|
70
|
+
@param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment
|
|
71
|
+
document from another source when in offlineMode. Works in place of
|
|
72
|
+
defaultFlagHandler if offlineMode is not set and using remote evaluation.
|
|
73
|
+
*/
|
|
74
|
+
constructor(data?: FlagsmithConfig);
|
|
67
75
|
/**
|
|
68
76
|
* Get all the default for flags for the current environment.
|
|
69
77
|
*
|
package/build/sdk/index.js
CHANGED
|
@@ -89,6 +89,7 @@ Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { r
|
|
|
89
89
|
var polling_manager_2 = require("./polling_manager");
|
|
90
90
|
Object.defineProperty(exports, "EnvironmentDataPollingManager", { enumerable: true, get: function () { return polling_manager_2.EnvironmentDataPollingManager; } });
|
|
91
91
|
var DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
|
|
92
|
+
var DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
|
|
92
93
|
var Flagsmith = /** @class */ (function () {
|
|
93
94
|
/**
|
|
94
95
|
* A Flagsmith client.
|
|
@@ -104,6 +105,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
104
105
|
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
105
106
|
*
|
|
106
107
|
* @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
|
|
108
|
+
* Required unless offlineMode is True.
|
|
107
109
|
@param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
|
|
108
110
|
@param data.customHeaders: Additional headers to add to requests made to the
|
|
109
111
|
Flagsmith API
|
|
@@ -117,33 +119,54 @@ var Flagsmith = /** @class */ (function () {
|
|
|
117
119
|
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
118
120
|
API to power flag analytics charts
|
|
119
121
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
120
|
-
flags cannot be retrieved from the API or a non
|
|
122
|
+
flags cannot be retrieved from the API or a non-existent feature is
|
|
121
123
|
requested
|
|
122
124
|
@param data.logger: an instance of the pino Logger class to use for logging
|
|
123
|
-
|
|
125
|
+
@param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for
|
|
126
|
+
evaluating flags.
|
|
127
|
+
@param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment
|
|
128
|
+
document from another source when in offlineMode. Works in place of
|
|
129
|
+
defaultFlagHandler if offlineMode is not set and using remote evaluation.
|
|
130
|
+
*/
|
|
124
131
|
function Flagsmith(data) {
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
// if (!data.offlineMode && !data.environmentKey) {
|
|
133
|
+
// throw new Error('ValueError: environmentKey is required.');
|
|
134
|
+
// }
|
|
135
|
+
if (data === void 0) { data = {}; }
|
|
136
|
+
var _a;
|
|
137
|
+
this.environmentKey = undefined;
|
|
138
|
+
this.apiUrl = undefined;
|
|
127
139
|
this.enableLocalEvaluation = false;
|
|
128
140
|
this.environmentRefreshIntervalSeconds = 60;
|
|
129
141
|
this.enableAnalytics = false;
|
|
142
|
+
this.offlineMode = false;
|
|
143
|
+
this.offlineHandler = undefined;
|
|
130
144
|
this.agent = data.agent;
|
|
131
145
|
this.environmentKey = data.environmentKey;
|
|
132
146
|
this.apiUrl = data.apiUrl || this.apiUrl;
|
|
133
147
|
this.customHeaders = data.customHeaders;
|
|
134
|
-
this.
|
|
135
|
-
|
|
148
|
+
this.requestTimeoutMs =
|
|
149
|
+
1000 * ((_a = data.requestTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_REQUEST_TIMEOUT_SECONDS);
|
|
136
150
|
this.enableLocalEvaluation = data.enableLocalEvaluation;
|
|
137
151
|
this.environmentRefreshIntervalSeconds =
|
|
138
152
|
data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds;
|
|
139
153
|
this.retries = data.retries;
|
|
140
154
|
this.enableAnalytics = data.enableAnalytics || false;
|
|
141
155
|
this.defaultFlagHandler = data.defaultFlagHandler;
|
|
142
|
-
this.environmentFlagsUrl = "".concat(this.apiUrl, "flags/");
|
|
143
|
-
this.identitiesUrl = "".concat(this.apiUrl, "identities/");
|
|
144
|
-
this.environmentUrl = "".concat(this.apiUrl, "environment-document/");
|
|
145
156
|
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
146
157
|
this.logger = data.logger || (0, pino_1.default)();
|
|
158
|
+
this.offlineMode = data.offlineMode || false;
|
|
159
|
+
this.offlineHandler = data.offlineHandler;
|
|
160
|
+
// argument validation
|
|
161
|
+
if (this.offlineMode && !this.offlineHandler) {
|
|
162
|
+
throw new Error('ValueError: offlineHandler must be provided to use offline mode.');
|
|
163
|
+
}
|
|
164
|
+
else if (this.defaultFlagHandler && this.offlineHandler) {
|
|
165
|
+
throw new Error('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
|
|
166
|
+
}
|
|
167
|
+
if (this.offlineHandler) {
|
|
168
|
+
this.environment = this.offlineHandler.getEnvironment();
|
|
169
|
+
}
|
|
147
170
|
if (!!data.cache) {
|
|
148
171
|
var missingMethods = ['has', 'get', 'set'].filter(function (method) { return data.cache && !data.cache[method]; });
|
|
149
172
|
if (missingMethods.length > 0) {
|
|
@@ -151,22 +174,32 @@ var Flagsmith = /** @class */ (function () {
|
|
|
151
174
|
}
|
|
152
175
|
this.cache = data.cache;
|
|
153
176
|
}
|
|
154
|
-
if (this.
|
|
155
|
-
if (!this.environmentKey
|
|
156
|
-
|
|
177
|
+
if (!this.offlineMode) {
|
|
178
|
+
if (!this.environmentKey) {
|
|
179
|
+
throw new Error('ValueError: environmentKey is required.');
|
|
180
|
+
}
|
|
181
|
+
var apiUrl = data.apiUrl || DEFAULT_API_URL;
|
|
182
|
+
this.apiUrl = apiUrl.endsWith('/') ? apiUrl : "".concat(apiUrl, "/");
|
|
183
|
+
this.environmentFlagsUrl = "".concat(this.apiUrl, "flags/");
|
|
184
|
+
this.identitiesUrl = "".concat(this.apiUrl, "identities/");
|
|
185
|
+
this.environmentUrl = "".concat(this.apiUrl, "environment-document/");
|
|
186
|
+
if (this.enableLocalEvaluation) {
|
|
187
|
+
if (!this.environmentKey.startsWith('ser.')) {
|
|
188
|
+
console.error('In order to use local evaluation, please generate a server key in the environment settings page.');
|
|
189
|
+
}
|
|
190
|
+
this.environmentDataPollingManager = new polling_manager_1.EnvironmentDataPollingManager(this, this.environmentRefreshIntervalSeconds);
|
|
191
|
+
this.environmentDataPollingManager.start();
|
|
192
|
+
this.updateEnvironment();
|
|
157
193
|
}
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
this.analyticsProcessor = data.enableAnalytics
|
|
195
|
+
? new analytics_1.AnalyticsProcessor({
|
|
196
|
+
environmentKey: this.environmentKey,
|
|
197
|
+
baseApiUrl: this.apiUrl,
|
|
198
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
199
|
+
logger: this.logger
|
|
200
|
+
})
|
|
201
|
+
: undefined;
|
|
161
202
|
}
|
|
162
|
-
this.analyticsProcessor = data.enableAnalytics
|
|
163
|
-
? new analytics_1.AnalyticsProcessor({
|
|
164
|
-
environmentKey: this.environmentKey,
|
|
165
|
-
baseApiUrl: this.apiUrl,
|
|
166
|
-
requestTimeoutMs: this.requestTimeoutMs,
|
|
167
|
-
logger: this.logger
|
|
168
|
-
})
|
|
169
|
-
: undefined;
|
|
170
203
|
}
|
|
171
204
|
/**
|
|
172
205
|
* Get all the default for flags for the current environment.
|
|
@@ -191,7 +224,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
191
224
|
if (!!cachedItem) {
|
|
192
225
|
return [2 /*return*/, cachedItem];
|
|
193
226
|
}
|
|
194
|
-
if (this.enableLocalEvaluation) {
|
|
227
|
+
if (this.enableLocalEvaluation && !this.offlineMode) {
|
|
195
228
|
return [2 /*return*/, new Promise(function (resolve, reject) {
|
|
196
229
|
return _this.environmentPromise.then(function () {
|
|
197
230
|
resolve(_this.getEnvironmentFlagsFromDocument());
|
|
@@ -225,7 +258,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
225
258
|
switch (_b.label) {
|
|
226
259
|
case 0:
|
|
227
260
|
if (!identifier) {
|
|
228
|
-
throw new Error(
|
|
261
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
229
262
|
}
|
|
230
263
|
_a = !!this.cache;
|
|
231
264
|
if (!_a) return [3 /*break*/, 2];
|
|
@@ -246,6 +279,9 @@ var Flagsmith = /** @class */ (function () {
|
|
|
246
279
|
}).catch(function (e) { return reject(e); });
|
|
247
280
|
})];
|
|
248
281
|
}
|
|
282
|
+
if (this.offlineMode) {
|
|
283
|
+
return [2 /*return*/, this.getIdentityFlagsFromDocument(identifier, traits || {})];
|
|
284
|
+
}
|
|
249
285
|
return [2 /*return*/, this.getIdentityFlagsFromApi(identifier, traits)];
|
|
250
286
|
}
|
|
251
287
|
});
|
|
@@ -265,7 +301,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
265
301
|
Flagsmith.prototype.getIdentitySegments = function (identifier, traits) {
|
|
266
302
|
var _this = this;
|
|
267
303
|
if (!identifier) {
|
|
268
|
-
throw new Error(
|
|
304
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
269
305
|
}
|
|
270
306
|
traits = traits || {};
|
|
271
307
|
if (this.enableLocalEvaluation) {
|
|
@@ -383,7 +419,11 @@ var Flagsmith = /** @class */ (function () {
|
|
|
383
419
|
var environment_data;
|
|
384
420
|
return __generator(this, function (_a) {
|
|
385
421
|
switch (_a.label) {
|
|
386
|
-
case 0:
|
|
422
|
+
case 0:
|
|
423
|
+
if (!this.environmentUrl) {
|
|
424
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
425
|
+
}
|
|
426
|
+
return [4 /*yield*/, this.getJSONResponse(this.environmentUrl, 'GET')];
|
|
387
427
|
case 1:
|
|
388
428
|
environment_data = _a.sent();
|
|
389
429
|
return [2 /*return*/, (0, util_1.buildEnvironmentModel)(environment_data)];
|
|
@@ -449,25 +489,33 @@ var Flagsmith = /** @class */ (function () {
|
|
|
449
489
|
return __generator(this, function (_a) {
|
|
450
490
|
switch (_a.label) {
|
|
451
491
|
case 0:
|
|
452
|
-
|
|
453
|
-
|
|
492
|
+
if (!this.environmentFlagsUrl) {
|
|
493
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
494
|
+
}
|
|
495
|
+
_a.label = 1;
|
|
454
496
|
case 1:
|
|
497
|
+
_a.trys.push([1, 5, , 6]);
|
|
498
|
+
return [4 /*yield*/, this.getJSONResponse(this.environmentFlagsUrl, 'GET')];
|
|
499
|
+
case 2:
|
|
455
500
|
apiFlags = _a.sent();
|
|
456
501
|
flags = models_3.Flags.fromAPIFlags({
|
|
457
502
|
apiFlags: apiFlags,
|
|
458
503
|
analyticsProcessor: this.analyticsProcessor,
|
|
459
504
|
defaultFlagHandler: this.defaultFlagHandler
|
|
460
505
|
});
|
|
461
|
-
if (!!!this.cache) return [3 /*break*/,
|
|
506
|
+
if (!!!this.cache) return [3 /*break*/, 4];
|
|
462
507
|
// @ts-ignore node-cache types are incorrect, ttl should be optional
|
|
463
508
|
return [4 /*yield*/, this.cache.set('flags', flags)];
|
|
464
|
-
case
|
|
509
|
+
case 3:
|
|
465
510
|
// @ts-ignore node-cache types are incorrect, ttl should be optional
|
|
466
511
|
_a.sent();
|
|
467
|
-
_a.label =
|
|
468
|
-
case
|
|
469
|
-
case
|
|
512
|
+
_a.label = 4;
|
|
513
|
+
case 4: return [2 /*return*/, flags];
|
|
514
|
+
case 5:
|
|
470
515
|
e_3 = _a.sent();
|
|
516
|
+
if (this.offlineHandler) {
|
|
517
|
+
return [2 /*return*/, this.getEnvironmentFlagsFromDocument()];
|
|
518
|
+
}
|
|
471
519
|
if (this.defaultFlagHandler) {
|
|
472
520
|
return [2 /*return*/, new models_3.Flags({
|
|
473
521
|
flags: {},
|
|
@@ -475,7 +523,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
475
523
|
})];
|
|
476
524
|
}
|
|
477
525
|
throw e_3;
|
|
478
|
-
case
|
|
526
|
+
case 6: return [2 /*return*/];
|
|
479
527
|
}
|
|
480
528
|
});
|
|
481
529
|
});
|
|
@@ -486,26 +534,34 @@ var Flagsmith = /** @class */ (function () {
|
|
|
486
534
|
return __generator(this, function (_a) {
|
|
487
535
|
switch (_a.label) {
|
|
488
536
|
case 0:
|
|
489
|
-
|
|
537
|
+
if (!this.identitiesUrl) {
|
|
538
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
539
|
+
}
|
|
540
|
+
_a.label = 1;
|
|
541
|
+
case 1:
|
|
542
|
+
_a.trys.push([1, 5, , 6]);
|
|
490
543
|
data = (0, utils_1.generateIdentitiesData)(identifier, traits);
|
|
491
544
|
return [4 /*yield*/, this.getJSONResponse(this.identitiesUrl, 'POST', data)];
|
|
492
|
-
case
|
|
545
|
+
case 2:
|
|
493
546
|
jsonResponse = _a.sent();
|
|
494
547
|
flags = models_3.Flags.fromAPIFlags({
|
|
495
548
|
apiFlags: jsonResponse['flags'],
|
|
496
549
|
analyticsProcessor: this.analyticsProcessor,
|
|
497
550
|
defaultFlagHandler: this.defaultFlagHandler
|
|
498
551
|
});
|
|
499
|
-
if (!!!this.cache) return [3 /*break*/,
|
|
552
|
+
if (!!!this.cache) return [3 /*break*/, 4];
|
|
500
553
|
// @ts-ignore node-cache types are incorrect, ttl should be optional
|
|
501
554
|
return [4 /*yield*/, this.cache.set("flags-".concat(identifier), flags)];
|
|
502
|
-
case
|
|
555
|
+
case 3:
|
|
503
556
|
// @ts-ignore node-cache types are incorrect, ttl should be optional
|
|
504
557
|
_a.sent();
|
|
505
|
-
_a.label =
|
|
506
|
-
case
|
|
507
|
-
case
|
|
558
|
+
_a.label = 4;
|
|
559
|
+
case 4: return [2 /*return*/, flags];
|
|
560
|
+
case 5:
|
|
508
561
|
e_4 = _a.sent();
|
|
562
|
+
if (this.offlineHandler) {
|
|
563
|
+
return [2 /*return*/, this.getIdentityFlagsFromDocument(identifier, traits)];
|
|
564
|
+
}
|
|
509
565
|
if (this.defaultFlagHandler) {
|
|
510
566
|
return [2 /*return*/, new models_3.Flags({
|
|
511
567
|
flags: {},
|
|
@@ -513,7 +569,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
513
569
|
})];
|
|
514
570
|
}
|
|
515
571
|
throw e_4;
|
|
516
|
-
case
|
|
572
|
+
case 6: return [2 /*return*/];
|
|
517
573
|
}
|
|
518
574
|
});
|
|
519
575
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EnvironmentModel } from '../flagsmith-engine/environments/models';
|
|
2
|
+
export declare class BaseOfflineHandler {
|
|
3
|
+
getEnvironment(): EnvironmentModel;
|
|
4
|
+
}
|
|
5
|
+
export declare class LocalFileHandler extends BaseOfflineHandler {
|
|
6
|
+
environment: EnvironmentModel;
|
|
7
|
+
constructor(environment_document_path: string);
|
|
8
|
+
getEnvironment(): EnvironmentModel;
|
|
9
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __extends = (this && this.__extends) || (function () {
|
|
3
|
+
var extendStatics = function (d, b) {
|
|
4
|
+
extendStatics = Object.setPrototypeOf ||
|
|
5
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
+
return extendStatics(d, b);
|
|
8
|
+
};
|
|
9
|
+
return function (d, b) {
|
|
10
|
+
if (typeof b !== "function" && b !== null)
|
|
11
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
+
extendStatics(d, b);
|
|
13
|
+
function __() { this.constructor = d; }
|
|
14
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
+
};
|
|
16
|
+
})();
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.LocalFileHandler = exports.BaseOfflineHandler = void 0;
|
|
42
|
+
var fs = __importStar(require("fs"));
|
|
43
|
+
var util_1 = require("../flagsmith-engine/environments/util");
|
|
44
|
+
var BaseOfflineHandler = /** @class */ (function () {
|
|
45
|
+
function BaseOfflineHandler() {
|
|
46
|
+
}
|
|
47
|
+
BaseOfflineHandler.prototype.getEnvironment = function () {
|
|
48
|
+
throw new Error('Not implemented');
|
|
49
|
+
};
|
|
50
|
+
return BaseOfflineHandler;
|
|
51
|
+
}());
|
|
52
|
+
exports.BaseOfflineHandler = BaseOfflineHandler;
|
|
53
|
+
var LocalFileHandler = /** @class */ (function (_super) {
|
|
54
|
+
__extends(LocalFileHandler, _super);
|
|
55
|
+
function LocalFileHandler(environment_document_path) {
|
|
56
|
+
var _this = _super.call(this) || this;
|
|
57
|
+
var environment_document = fs.readFileSync(environment_document_path, 'utf8');
|
|
58
|
+
_this.environment = (0, util_1.buildEnvironmentModel)(JSON.parse(environment_document));
|
|
59
|
+
return _this;
|
|
60
|
+
}
|
|
61
|
+
LocalFileHandler.prototype.getEnvironment = function () {
|
|
62
|
+
return this.environment;
|
|
63
|
+
};
|
|
64
|
+
return LocalFileHandler;
|
|
65
|
+
}(BaseOfflineHandler));
|
|
66
|
+
exports.LocalFileHandler = LocalFileHandler;
|
package/build/sdk/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DefaultFlag, Flags } from "./models";
|
|
|
2
2
|
import { EnvironmentModel } from "../flagsmith-engine";
|
|
3
3
|
import { RequestInit } from "node-fetch";
|
|
4
4
|
import { Logger } from "pino";
|
|
5
|
+
import { BaseOfflineHandler } from "./offline_handlers";
|
|
5
6
|
export interface FlagsmithCache {
|
|
6
7
|
get(key: string): Promise<Flags | undefined> | undefined;
|
|
7
8
|
set(key: string, value: Flags, ttl: string | number): boolean | Promise<boolean>;
|
|
@@ -9,7 +10,7 @@ export interface FlagsmithCache {
|
|
|
9
10
|
[key: string]: any;
|
|
10
11
|
}
|
|
11
12
|
export interface FlagsmithConfig {
|
|
12
|
-
environmentKey
|
|
13
|
+
environmentKey?: string;
|
|
13
14
|
apiUrl?: string;
|
|
14
15
|
agent?: RequestInit['agent'];
|
|
15
16
|
customHeaders?: {
|
|
@@ -24,4 +25,6 @@ export interface FlagsmithConfig {
|
|
|
24
25
|
cache?: FlagsmithCache;
|
|
25
26
|
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
|
|
26
27
|
logger?: Logger;
|
|
28
|
+
offlineMode?: boolean;
|
|
29
|
+
offlineHandler?: BaseOfflineHandler;
|
|
27
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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": {
|
package/sdk/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RequestInit } from
|
|
1
|
+
import { RequestInit } from 'node-fetch';
|
|
2
2
|
import { getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine';
|
|
3
3
|
import { EnvironmentModel } from '../flagsmith-engine/environments/models';
|
|
4
4
|
import { buildEnvironmentModel } from '../flagsmith-engine/environments/util';
|
|
@@ -6,6 +6,7 @@ import { IdentityModel } from '../flagsmith-engine/identities/models';
|
|
|
6
6
|
import { TraitModel } from '../flagsmith-engine/identities/traits/models';
|
|
7
7
|
|
|
8
8
|
import { AnalyticsProcessor } from './analytics';
|
|
9
|
+
import { BaseOfflineHandler } from './offline_handlers';
|
|
9
10
|
import { FlagsmithAPIError, FlagsmithClientError } from './errors';
|
|
10
11
|
|
|
11
12
|
import { DefaultFlag, Flags } from './models';
|
|
@@ -14,7 +15,7 @@ import { generateIdentitiesData, retryFetch } from './utils';
|
|
|
14
15
|
import { SegmentModel } from '../flagsmith-engine/segments/models';
|
|
15
16
|
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators';
|
|
16
17
|
import { FlagsmithCache, FlagsmithConfig } from './types';
|
|
17
|
-
import pino, { Logger } from
|
|
18
|
+
import pino, { Logger } from 'pino';
|
|
18
19
|
|
|
19
20
|
export { AnalyticsProcessor } from './analytics';
|
|
20
21
|
export { FlagsmithAPIError, FlagsmithClientError } from './errors';
|
|
@@ -24,13 +25,12 @@ export { EnvironmentDataPollingManager } from './polling_manager';
|
|
|
24
25
|
export { FlagsmithCache, FlagsmithConfig } from './types';
|
|
25
26
|
|
|
26
27
|
const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
|
|
27
|
-
|
|
28
|
+
const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
|
|
28
29
|
|
|
29
30
|
export class Flagsmith {
|
|
30
|
-
environmentKey?: string;
|
|
31
|
-
apiUrl
|
|
31
|
+
environmentKey?: string = undefined;
|
|
32
|
+
apiUrl?: string = undefined;
|
|
32
33
|
customHeaders?: { [key: string]: any };
|
|
33
|
-
requestTimeoutSeconds?: number = 10;
|
|
34
34
|
agent: RequestInit['agent'];
|
|
35
35
|
requestTimeoutMs?: number;
|
|
36
36
|
enableLocalEvaluation?: boolean = false;
|
|
@@ -40,12 +40,14 @@ export class Flagsmith {
|
|
|
40
40
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
environmentFlagsUrl
|
|
44
|
-
identitiesUrl
|
|
45
|
-
environmentUrl
|
|
43
|
+
environmentFlagsUrl?: string;
|
|
44
|
+
identitiesUrl?: string;
|
|
45
|
+
environmentUrl?: string;
|
|
46
46
|
|
|
47
47
|
environmentDataPollingManager?: EnvironmentDataPollingManager;
|
|
48
48
|
environment!: EnvironmentModel;
|
|
49
|
+
offlineMode: boolean = false;
|
|
50
|
+
offlineHandler?: BaseOfflineHandler = undefined;
|
|
49
51
|
|
|
50
52
|
private cache?: FlagsmithCache;
|
|
51
53
|
private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
|
|
@@ -65,6 +67,7 @@ export class Flagsmith {
|
|
|
65
67
|
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
66
68
|
*
|
|
67
69
|
* @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
|
|
70
|
+
* Required unless offlineMode is True.
|
|
68
71
|
@param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
|
|
69
72
|
@param data.customHeaders: Additional headers to add to requests made to the
|
|
70
73
|
Flagsmith API
|
|
@@ -78,17 +81,26 @@ export class Flagsmith {
|
|
|
78
81
|
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
79
82
|
API to power flag analytics charts
|
|
80
83
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
81
|
-
flags cannot be retrieved from the API or a non
|
|
84
|
+
flags cannot be retrieved from the API or a non-existent feature is
|
|
82
85
|
requested
|
|
83
86
|
@param data.logger: an instance of the pino Logger class to use for logging
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
@param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for
|
|
88
|
+
evaluating flags.
|
|
89
|
+
@param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment
|
|
90
|
+
document from another source when in offlineMode. Works in place of
|
|
91
|
+
defaultFlagHandler if offlineMode is not set and using remote evaluation.
|
|
92
|
+
*/
|
|
93
|
+
constructor(data: FlagsmithConfig = {}) {
|
|
94
|
+
// if (!data.offlineMode && !data.environmentKey) {
|
|
95
|
+
// throw new Error('ValueError: environmentKey is required.');
|
|
96
|
+
// }
|
|
97
|
+
|
|
86
98
|
this.agent = data.agent;
|
|
87
99
|
this.environmentKey = data.environmentKey;
|
|
88
100
|
this.apiUrl = data.apiUrl || this.apiUrl;
|
|
89
101
|
this.customHeaders = data.customHeaders;
|
|
90
|
-
this.
|
|
91
|
-
|
|
102
|
+
this.requestTimeoutMs =
|
|
103
|
+
1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
|
|
92
104
|
this.enableLocalEvaluation = data.enableLocalEvaluation;
|
|
93
105
|
this.environmentRefreshIntervalSeconds =
|
|
94
106
|
data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds;
|
|
@@ -96,14 +108,26 @@ export class Flagsmith {
|
|
|
96
108
|
this.enableAnalytics = data.enableAnalytics || false;
|
|
97
109
|
this.defaultFlagHandler = data.defaultFlagHandler;
|
|
98
110
|
|
|
99
|
-
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
|
|
100
|
-
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
101
|
-
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
102
111
|
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
103
112
|
this.logger = data.logger || pino();
|
|
113
|
+
this.offlineMode = data.offlineMode || false;
|
|
114
|
+
this.offlineHandler = data.offlineHandler;
|
|
115
|
+
|
|
116
|
+
// argument validation
|
|
117
|
+
if (this.offlineMode && !this.offlineHandler) {
|
|
118
|
+
throw new Error('ValueError: offlineHandler must be provided to use offline mode.');
|
|
119
|
+
} else if (this.defaultFlagHandler && this.offlineHandler) {
|
|
120
|
+
throw new Error('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.offlineHandler) {
|
|
124
|
+
this.environment = this.offlineHandler.getEnvironment();
|
|
125
|
+
}
|
|
104
126
|
|
|
105
127
|
if (!!data.cache) {
|
|
106
|
-
const missingMethods: string[] = ['has', 'get', 'set'].filter(
|
|
128
|
+
const missingMethods: string[] = ['has', 'get', 'set'].filter(
|
|
129
|
+
method => data.cache && !data.cache[method]
|
|
130
|
+
);
|
|
107
131
|
|
|
108
132
|
if (missingMethods.length > 0) {
|
|
109
133
|
throw new Error(
|
|
@@ -115,28 +139,40 @@ export class Flagsmith {
|
|
|
115
139
|
this.cache = data.cache;
|
|
116
140
|
}
|
|
117
141
|
|
|
118
|
-
if (this.
|
|
119
|
-
if (!this.environmentKey
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
if (!this.offlineMode) {
|
|
143
|
+
if (!this.environmentKey) {
|
|
144
|
+
throw new Error('ValueError: environmentKey is required.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const apiUrl = data.apiUrl || DEFAULT_API_URL;
|
|
148
|
+
this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
|
149
|
+
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
|
|
150
|
+
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
151
|
+
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
152
|
+
|
|
153
|
+
if (this.enableLocalEvaluation) {
|
|
154
|
+
if (!this.environmentKey.startsWith('ser.')) {
|
|
155
|
+
console.error(
|
|
156
|
+
'In order to use local evaluation, please generate a server key in the environment settings page.'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
160
|
+
this,
|
|
161
|
+
this.environmentRefreshIntervalSeconds
|
|
122
162
|
);
|
|
163
|
+
this.environmentDataPollingManager.start();
|
|
164
|
+
this.updateEnvironment();
|
|
123
165
|
}
|
|
124
|
-
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
125
|
-
this,
|
|
126
|
-
this.environmentRefreshIntervalSeconds
|
|
127
|
-
);
|
|
128
|
-
this.environmentDataPollingManager.start();
|
|
129
|
-
this.updateEnvironment();
|
|
130
|
-
}
|
|
131
166
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
this.analyticsProcessor = data.enableAnalytics
|
|
168
|
+
? new AnalyticsProcessor({
|
|
169
|
+
environmentKey: this.environmentKey,
|
|
170
|
+
baseApiUrl: this.apiUrl,
|
|
171
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
172
|
+
logger: this.logger
|
|
173
|
+
})
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
140
176
|
}
|
|
141
177
|
/**
|
|
142
178
|
* Get all the default for flags for the current environment.
|
|
@@ -144,15 +180,15 @@ export class Flagsmith {
|
|
|
144
180
|
* @returns Flags object holding all the flags for the current environment.
|
|
145
181
|
*/
|
|
146
182
|
async getEnvironmentFlags(): Promise<Flags> {
|
|
147
|
-
const cachedItem = !!this.cache && await this.cache.get(`flags`);
|
|
183
|
+
const cachedItem = !!this.cache && (await this.cache.get(`flags`));
|
|
148
184
|
if (!!cachedItem) {
|
|
149
185
|
return cachedItem;
|
|
150
186
|
}
|
|
151
|
-
if (this.enableLocalEvaluation) {
|
|
187
|
+
if (this.enableLocalEvaluation && !this.offlineMode) {
|
|
152
188
|
return new Promise((resolve, reject) =>
|
|
153
189
|
this.environmentPromise!.then(() => {
|
|
154
190
|
resolve(this.getEnvironmentFlagsFromDocument());
|
|
155
|
-
}).catch(
|
|
191
|
+
}).catch(e => reject(e))
|
|
156
192
|
);
|
|
157
193
|
}
|
|
158
194
|
if (this.environment) {
|
|
@@ -161,6 +197,7 @@ export class Flagsmith {
|
|
|
161
197
|
|
|
162
198
|
return this.getEnvironmentFlagsFromApi();
|
|
163
199
|
}
|
|
200
|
+
|
|
164
201
|
/**
|
|
165
202
|
* Get all the flags for the current environment for a given identity. Will also
|
|
166
203
|
upsert all traits to the Flagsmith API for future evaluations. Providing a
|
|
@@ -174,10 +211,10 @@ export class Flagsmith {
|
|
|
174
211
|
*/
|
|
175
212
|
async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
|
|
176
213
|
if (!identifier) {
|
|
177
|
-
throw new Error(
|
|
214
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
|
|
217
|
+
const cachedItem = !!this.cache && (await this.cache.get(`flags-${identifier}`));
|
|
181
218
|
if (!!cachedItem) {
|
|
182
219
|
return cachedItem;
|
|
183
220
|
}
|
|
@@ -189,6 +226,10 @@ export class Flagsmith {
|
|
|
189
226
|
}).catch(e => reject(e))
|
|
190
227
|
);
|
|
191
228
|
}
|
|
229
|
+
if (this.offlineMode) {
|
|
230
|
+
return this.getIdentityFlagsFromDocument(identifier, traits || {});
|
|
231
|
+
}
|
|
232
|
+
|
|
192
233
|
return this.getIdentityFlagsFromApi(identifier, traits);
|
|
193
234
|
}
|
|
194
235
|
|
|
@@ -208,7 +249,7 @@ export class Flagsmith {
|
|
|
208
249
|
traits?: { [key: string]: any }
|
|
209
250
|
): Promise<SegmentModel[]> {
|
|
210
251
|
if (!identifier) {
|
|
211
|
-
throw new Error(
|
|
252
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
212
253
|
}
|
|
213
254
|
|
|
214
255
|
traits = traits || {};
|
|
@@ -225,7 +266,7 @@ export class Flagsmith {
|
|
|
225
266
|
|
|
226
267
|
const segments = getIdentitySegments(this.environment, identityModel);
|
|
227
268
|
return resolve(segments);
|
|
228
|
-
}).catch(
|
|
269
|
+
}).catch(e => reject(e));
|
|
229
270
|
});
|
|
230
271
|
}
|
|
231
272
|
console.error('This function is only permitted with local evaluation.');
|
|
@@ -287,7 +328,7 @@ export class Flagsmith {
|
|
|
287
328
|
headers: headers
|
|
288
329
|
},
|
|
289
330
|
this.retries,
|
|
290
|
-
this.requestTimeoutMs || undefined
|
|
331
|
+
this.requestTimeoutMs || undefined
|
|
291
332
|
);
|
|
292
333
|
|
|
293
334
|
if (data.status !== 200) {
|
|
@@ -305,6 +346,9 @@ export class Flagsmith {
|
|
|
305
346
|
private environmentPromise: Promise<any> | undefined;
|
|
306
347
|
|
|
307
348
|
private async getEnvironmentFromApi() {
|
|
349
|
+
if (!this.environmentUrl) {
|
|
350
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
351
|
+
}
|
|
308
352
|
const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
|
|
309
353
|
return buildEnvironmentModel(environment_data);
|
|
310
354
|
}
|
|
@@ -322,7 +366,10 @@ export class Flagsmith {
|
|
|
322
366
|
return flags;
|
|
323
367
|
}
|
|
324
368
|
|
|
325
|
-
private async getIdentityFlagsFromDocument(
|
|
369
|
+
private async getIdentityFlagsFromDocument(
|
|
370
|
+
identifier: string,
|
|
371
|
+
traits: { [key: string]: any }
|
|
372
|
+
): Promise<Flags> {
|
|
326
373
|
const identityModel = this.buildIdentityModel(
|
|
327
374
|
identifier,
|
|
328
375
|
Object.keys(traits).map(key => ({
|
|
@@ -349,6 +396,9 @@ export class Flagsmith {
|
|
|
349
396
|
}
|
|
350
397
|
|
|
351
398
|
private async getEnvironmentFlagsFromApi() {
|
|
399
|
+
if (!this.environmentFlagsUrl) {
|
|
400
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
401
|
+
}
|
|
352
402
|
try {
|
|
353
403
|
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
354
404
|
const flags = Flags.fromAPIFlags({
|
|
@@ -362,6 +412,9 @@ export class Flagsmith {
|
|
|
362
412
|
}
|
|
363
413
|
return flags;
|
|
364
414
|
} catch (e) {
|
|
415
|
+
if (this.offlineHandler) {
|
|
416
|
+
return this.getEnvironmentFlagsFromDocument();
|
|
417
|
+
}
|
|
365
418
|
if (this.defaultFlagHandler) {
|
|
366
419
|
return new Flags({
|
|
367
420
|
flags: {},
|
|
@@ -374,6 +427,9 @@ export class Flagsmith {
|
|
|
374
427
|
}
|
|
375
428
|
|
|
376
429
|
private async getIdentityFlagsFromApi(identifier: string, traits: { [key: string]: any }) {
|
|
430
|
+
if (!this.identitiesUrl) {
|
|
431
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
432
|
+
}
|
|
377
433
|
try {
|
|
378
434
|
const data = generateIdentitiesData(identifier, traits);
|
|
379
435
|
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
@@ -388,6 +444,9 @@ export class Flagsmith {
|
|
|
388
444
|
}
|
|
389
445
|
return flags;
|
|
390
446
|
} catch (e) {
|
|
447
|
+
if (this.offlineHandler) {
|
|
448
|
+
return this.getIdentityFlagsFromDocument(identifier, traits);
|
|
449
|
+
}
|
|
391
450
|
if (this.defaultFlagHandler) {
|
|
392
451
|
return new Flags({
|
|
393
452
|
flags: {},
|
|
@@ -406,4 +465,3 @@ export class Flagsmith {
|
|
|
406
465
|
}
|
|
407
466
|
|
|
408
467
|
export default Flagsmith;
|
|
409
|
-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { buildEnvironmentModel } from '../flagsmith-engine/environments/util';
|
|
3
|
+
import { EnvironmentModel } from '../flagsmith-engine/environments/models';
|
|
4
|
+
|
|
5
|
+
export class BaseOfflineHandler {
|
|
6
|
+
getEnvironment() : EnvironmentModel {
|
|
7
|
+
throw new Error('Not implemented');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class LocalFileHandler extends BaseOfflineHandler {
|
|
12
|
+
environment: EnvironmentModel;
|
|
13
|
+
constructor(environment_document_path: string) {
|
|
14
|
+
super();
|
|
15
|
+
const environment_document = fs.readFileSync(environment_document_path, 'utf8');
|
|
16
|
+
this.environment = buildEnvironmentModel(JSON.parse(environment_document));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getEnvironment(): EnvironmentModel {
|
|
20
|
+
return this.environment;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/sdk/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DefaultFlag, Flags } from "./models";
|
|
|
2
2
|
import { EnvironmentModel } from "../flagsmith-engine";
|
|
3
3
|
import { RequestInit } from "node-fetch";
|
|
4
4
|
import { Logger } from "pino";
|
|
5
|
+
import { BaseOfflineHandler } from "./offline_handlers";
|
|
5
6
|
|
|
6
7
|
export interface FlagsmithCache {
|
|
7
8
|
get(key: string): Promise<Flags|undefined> | undefined;
|
|
@@ -11,7 +12,7 @@ export interface FlagsmithCache {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface FlagsmithConfig {
|
|
14
|
-
environmentKey
|
|
15
|
+
environmentKey?: string;
|
|
15
16
|
apiUrl?: string;
|
|
16
17
|
agent?:RequestInit['agent'];
|
|
17
18
|
customHeaders?: { [key: string]: any };
|
|
@@ -24,4 +25,6 @@ export interface FlagsmithConfig {
|
|
|
24
25
|
cache?: FlagsmithCache,
|
|
25
26
|
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void,
|
|
26
27
|
logger?: Logger
|
|
28
|
+
offlineMode?: boolean;
|
|
29
|
+
offlineHandler?: BaseOfflineHandler;
|
|
27
30
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api_key": "B62qaMZNwfiqT76p38ggrQ",
|
|
3
|
+
"project": {
|
|
4
|
+
"name": "Test project",
|
|
5
|
+
"organisation": {
|
|
6
|
+
"feature_analytics": false,
|
|
7
|
+
"name": "Test Org",
|
|
8
|
+
"id": 1,
|
|
9
|
+
"persist_trait_data": true,
|
|
10
|
+
"stop_serving_flags": false
|
|
11
|
+
},
|
|
12
|
+
"id": 1,
|
|
13
|
+
"hide_disabled_flags": false,
|
|
14
|
+
"segments": [
|
|
15
|
+
{
|
|
16
|
+
"name": "regular_segment",
|
|
17
|
+
"feature_states": [
|
|
18
|
+
{
|
|
19
|
+
"feature_state_value": "segment_override",
|
|
20
|
+
"multivariate_feature_state_values": [],
|
|
21
|
+
"django_id": 81027,
|
|
22
|
+
"feature": {
|
|
23
|
+
"name": "some_feature",
|
|
24
|
+
"type": "STANDARD",
|
|
25
|
+
"id": 1
|
|
26
|
+
},
|
|
27
|
+
"enabled": false
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"id": 1,
|
|
31
|
+
"rules": [
|
|
32
|
+
{
|
|
33
|
+
"type": "ALL",
|
|
34
|
+
"conditions": [],
|
|
35
|
+
"rules": [
|
|
36
|
+
{
|
|
37
|
+
"type": "ANY",
|
|
38
|
+
"conditions": [
|
|
39
|
+
{
|
|
40
|
+
"value": "40",
|
|
41
|
+
"property_": "age",
|
|
42
|
+
"operator": "LESS_THAN"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"rules": []
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"segment_overrides": [],
|
|
54
|
+
"id": 1,
|
|
55
|
+
"feature_states": [
|
|
56
|
+
{
|
|
57
|
+
"multivariate_feature_state_values": [],
|
|
58
|
+
"feature_state_value": "offline-value",
|
|
59
|
+
"id": 1,
|
|
60
|
+
"featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
|
|
61
|
+
"feature": {
|
|
62
|
+
"name": "some_feature",
|
|
63
|
+
"type": "STANDARD",
|
|
64
|
+
"id": 1
|
|
65
|
+
},
|
|
66
|
+
"feature_segment": null,
|
|
67
|
+
"enabled": true
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"multivariate_feature_state_values": [
|
|
71
|
+
{
|
|
72
|
+
"percentage_allocation": 100,
|
|
73
|
+
"multivariate_feature_option": {
|
|
74
|
+
"value": "bar",
|
|
75
|
+
"id": 1
|
|
76
|
+
},
|
|
77
|
+
"mv_fs_value_uuid": "42d5cdf9-8ec9-4b8d-a3ca-fd43c64d5f05",
|
|
78
|
+
"id": 1
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"feature_state_value": "foo",
|
|
82
|
+
"feature": {
|
|
83
|
+
"name": "mv_feature",
|
|
84
|
+
"type": "MULTIVARIATE",
|
|
85
|
+
"id": 2
|
|
86
|
+
},
|
|
87
|
+
"feature_segment": null,
|
|
88
|
+
"featurestate_uuid": "96fc3503-09d7-48f1-a83b-2dc903d5c08a",
|
|
89
|
+
"enabled": false
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -2,11 +2,12 @@ import Flagsmith from '../../sdk';
|
|
|
2
2
|
import { EnvironmentDataPollingManager } from '../../sdk/polling_manager';
|
|
3
3
|
import fetch, {RequestInit} from 'node-fetch';
|
|
4
4
|
import { environmentJSON, environmentModel, flagsJSON, flagsmith, identitiesJSON } from './utils';
|
|
5
|
-
import { DefaultFlag } from '../../sdk/models';
|
|
5
|
+
import { DefaultFlag, Flags } from '../../sdk/models';
|
|
6
6
|
import {delay, retryFetch} from '../../sdk/utils';
|
|
7
7
|
import * as utils from '../../sdk/utils';
|
|
8
8
|
import { EnvironmentModel } from '../../flagsmith-engine/environments/models';
|
|
9
9
|
import https from 'https'
|
|
10
|
+
import { BaseOfflineHandler } from '../../sdk/offline_handlers';
|
|
10
11
|
|
|
11
12
|
jest.mock('node-fetch');
|
|
12
13
|
jest.mock('../../sdk/polling_manager');
|
|
@@ -183,7 +184,7 @@ test('default flag handler used when timeout occurs', async () => {
|
|
|
183
184
|
const flg = new Flagsmith({
|
|
184
185
|
environmentKey: 'key',
|
|
185
186
|
defaultFlagHandler: defaultFlagHandler,
|
|
186
|
-
requestTimeoutSeconds: 0.
|
|
187
|
+
requestTimeoutSeconds: 0.001,
|
|
187
188
|
});
|
|
188
189
|
|
|
189
190
|
const flags = await flg.getEnvironmentFlags();
|
|
@@ -193,7 +194,16 @@ test('default flag handler used when timeout occurs', async () => {
|
|
|
193
194
|
expect(flag.value).toBe(defaultFlag.value);
|
|
194
195
|
})
|
|
195
196
|
|
|
196
|
-
test('
|
|
197
|
+
test('request timeout uses default if not provided', async () => {
|
|
198
|
+
|
|
199
|
+
const flg = new Flagsmith({
|
|
200
|
+
environmentKey: 'key',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(flg.requestTimeoutMs).toBe(10000);
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('test_throws_when_no_identityFlags_returned_due_to_error', async () => {
|
|
197
207
|
// @ts-ignore
|
|
198
208
|
fetch.mockReturnValue(Promise.resolve(new Response('bad data')));
|
|
199
209
|
|
|
@@ -266,10 +276,108 @@ test('getIdentitySegments throws error if identifier is empty string', () => {
|
|
|
266
276
|
})
|
|
267
277
|
|
|
268
278
|
|
|
269
|
-
async
|
|
279
|
+
test('offline_mode', async() => {
|
|
280
|
+
// Given
|
|
281
|
+
const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));
|
|
282
|
+
|
|
283
|
+
class DummyOfflineHandler extends BaseOfflineHandler {
|
|
284
|
+
getEnvironment(): EnvironmentModel {
|
|
285
|
+
return environment;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// When
|
|
290
|
+
const flagsmith = new Flagsmith({ offlineMode: true, offlineHandler: new DummyOfflineHandler() });
|
|
291
|
+
|
|
292
|
+
// Then
|
|
293
|
+
// we can request the flags from the client successfully
|
|
294
|
+
const environmentFlags: Flags = await flagsmith.getEnvironmentFlags();
|
|
295
|
+
let flag = environmentFlags.getFlag('some_feature');
|
|
296
|
+
expect(flag.isDefault).toBe(false);
|
|
297
|
+
expect(flag.enabled).toBe(true);
|
|
298
|
+
expect(flag.value).toBe('offline-value');
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
const identityFlags: Flags = await flagsmith.getIdentityFlags("identity");
|
|
302
|
+
flag = identityFlags.getFlag('some_feature');
|
|
303
|
+
expect(flag.isDefault).toBe(false);
|
|
304
|
+
expect(flag.enabled).toBe(true);
|
|
305
|
+
expect(flag.value).toBe('offline-value');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async () => {
|
|
310
|
+
// Given
|
|
311
|
+
const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));
|
|
312
|
+
const api_url = 'http://some.flagsmith.com/api/v1/';
|
|
313
|
+
const mock_offline_handler = new BaseOfflineHandler() as jest.Mocked<BaseOfflineHandler>;
|
|
314
|
+
|
|
315
|
+
jest.spyOn(mock_offline_handler, 'getEnvironment').mockReturnValue(environment);
|
|
316
|
+
|
|
317
|
+
const flagsmith = new Flagsmith({
|
|
318
|
+
environmentKey: 'some-key',
|
|
319
|
+
apiUrl: api_url,
|
|
320
|
+
offlineHandler: mock_offline_handler,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
jest.spyOn(flagsmith, 'getEnvironmentFlags');
|
|
324
|
+
jest.spyOn(flagsmith, 'getIdentityFlags');
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
flagsmith.environmentFlagsUrl = 'http://some.flagsmith.com/api/v1/environment-flags';
|
|
328
|
+
flagsmith.identitiesUrl = 'http://some.flagsmith.com/api/v1/identities';
|
|
329
|
+
|
|
330
|
+
// Mock a 500 Internal Server Error response
|
|
331
|
+
const errorResponse = new Response(null, {
|
|
332
|
+
status: 500,
|
|
333
|
+
statusText: 'Internal Server Error',
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// @ts-ignore
|
|
337
|
+
fetch.mockReturnValue(Promise.resolve(errorResponse));
|
|
338
|
+
|
|
339
|
+
// When
|
|
340
|
+
const environmentFlags:Flags = await flagsmith.getEnvironmentFlags();
|
|
341
|
+
const identityFlags:Flags = await flagsmith.getIdentityFlags('identity', {});
|
|
342
|
+
|
|
343
|
+
// Then
|
|
344
|
+
expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1);
|
|
345
|
+
expect(flagsmith.getEnvironmentFlags).toHaveBeenCalled();
|
|
346
|
+
expect(flagsmith.getIdentityFlags).toHaveBeenCalled();
|
|
347
|
+
|
|
348
|
+
expect(environmentFlags.isFeatureEnabled('some_feature')).toBe(true);
|
|
349
|
+
expect(environmentFlags.getFeatureValue('some_feature')).toBe('offline-value');
|
|
350
|
+
|
|
351
|
+
expect(identityFlags.isFeatureEnabled('some_feature')).toBe(true);
|
|
352
|
+
expect(identityFlags.getFeatureValue('some_feature')).toBe('offline-value');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('cannot use offline mode without offline handler', () => {
|
|
356
|
+
// When and Then
|
|
357
|
+
expect(() => new Flagsmith({ offlineMode: true, offlineHandler: undefined })).toThrowError(
|
|
358
|
+
'ValueError: offlineHandler must be provided to use offline mode.'
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('cannot use both default handler and offline handler', () => {
|
|
363
|
+
// When and Then
|
|
364
|
+
expect(() => new Flagsmith({
|
|
365
|
+
offlineHandler: new BaseOfflineHandler(),
|
|
366
|
+
defaultFlagHandler: (flagName) => new DefaultFlag('foo', true)
|
|
367
|
+
})).toThrowError('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('cannot create Flagsmith client in remote evaluation without API key', () => {
|
|
371
|
+
// When and Then
|
|
372
|
+
// @ts-ignore
|
|
373
|
+
expect(() => new Flagsmith()).toThrowError('ValueError: environmentKey is required.');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
async function wipeFeatureStateUUIDs (environmentModel: EnvironmentModel) {
|
|
270
378
|
// TODO: this has been pulled out of tests above as a helper function.
|
|
271
379
|
// I'm not entirely sure why it's necessary, however, we should look to remove.
|
|
272
|
-
|
|
380
|
+
environmentModel.featureStates.forEach(fs => {
|
|
273
381
|
// @ts-ignore
|
|
274
382
|
fs.featurestateUUID = undefined;
|
|
275
383
|
fs.multivariateFeatureStateValues.forEach(mvfsv => {
|
|
@@ -277,7 +385,7 @@ async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|
|
|
277
385
|
mvfsv.mvFsValueUuid = undefined;
|
|
278
386
|
})
|
|
279
387
|
});
|
|
280
|
-
|
|
388
|
+
environmentModel.project.segments.forEach(s => {
|
|
281
389
|
s.featureStates.forEach(fs => {
|
|
282
390
|
// @ts-ignore
|
|
283
391
|
fs.featurestateUUID = undefined;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { LocalFileHandler } from '../../sdk/offline_handlers';
|
|
3
|
+
import { EnvironmentModel } from '../../flagsmith-engine';
|
|
4
|
+
|
|
5
|
+
const offlineEnvironment = require('./data/offline-environment.json');
|
|
6
|
+
|
|
7
|
+
jest.mock('fs')
|
|
8
|
+
|
|
9
|
+
const offlineEnvironmentString = JSON.stringify(offlineEnvironment)
|
|
10
|
+
|
|
11
|
+
test('local file handler', () => {
|
|
12
|
+
const environmentDocumentFilePath = '/some/path/environment.json';
|
|
13
|
+
|
|
14
|
+
// Mock the fs.readFileSync function to return environmentJson
|
|
15
|
+
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
const readFileSyncMock = jest.spyOn(fs, 'readFileSync');
|
|
18
|
+
readFileSyncMock.mockImplementation(() => offlineEnvironmentString);
|
|
19
|
+
|
|
20
|
+
// Given
|
|
21
|
+
const localFileHandler = new LocalFileHandler(environmentDocumentFilePath);
|
|
22
|
+
|
|
23
|
+
// When
|
|
24
|
+
const environmentModel = localFileHandler.getEnvironment();
|
|
25
|
+
|
|
26
|
+
// Then
|
|
27
|
+
expect(environmentModel).toBeInstanceOf(EnvironmentModel);
|
|
28
|
+
expect(environmentModel.apiKey).toBe('B62qaMZNwfiqT76p38ggrQ');
|
|
29
|
+
expect(readFileSyncMock).toHaveBeenCalledWith(environmentDocumentFilePath, 'utf8');
|
|
30
|
+
|
|
31
|
+
// Restore the original implementation of fs.readFileSync
|
|
32
|
+
readFileSyncMock.mockRestore();
|
|
33
|
+
});
|
package/tests/sdk/utils.ts
CHANGED
|
@@ -42,8 +42,8 @@ export function flagsmith(params = {}) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function environmentJSON() {
|
|
46
|
-
return readFileSync(DATA_DIR +
|
|
45
|
+
export function environmentJSON(environmentFilename: string = 'environment.json') {
|
|
46
|
+
return readFileSync(DATA_DIR + environmentFilename, 'utf-8');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function environmentModel(environmentJSON: any) {
|