flagsmith-nodejs 3.1.1 → 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 -8
- package/build/sdk/index.js +97 -41
- 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 +104 -45
- 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 +104 -5
- 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,7 +12,7 @@ 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
|
};
|
|
@@ -22,11 +23,13 @@ export declare class Flagsmith {
|
|
|
22
23
|
retries?: number;
|
|
23
24
|
enableAnalytics: boolean;
|
|
24
25
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
25
|
-
environmentFlagsUrl
|
|
26
|
-
identitiesUrl
|
|
27
|
-
environmentUrl
|
|
26
|
+
environmentFlagsUrl?: string;
|
|
27
|
+
identitiesUrl?: string;
|
|
28
|
+
environmentUrl?: string;
|
|
28
29
|
environmentDataPollingManager?: EnvironmentDataPollingManager;
|
|
29
30
|
environment: EnvironmentModel;
|
|
31
|
+
offlineMode: boolean;
|
|
32
|
+
offlineHandler?: BaseOfflineHandler;
|
|
30
33
|
private cache?;
|
|
31
34
|
private onEnvironmentChange?;
|
|
32
35
|
private analyticsProcessor?;
|
|
@@ -45,6 +48,7 @@ export declare class Flagsmith {
|
|
|
45
48
|
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
46
49
|
*
|
|
47
50
|
* @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
|
|
51
|
+
* Required unless offlineMode is True.
|
|
48
52
|
@param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
|
|
49
53
|
@param data.customHeaders: Additional headers to add to requests made to the
|
|
50
54
|
Flagsmith API
|
|
@@ -58,11 +62,16 @@ export declare class Flagsmith {
|
|
|
58
62
|
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
59
63
|
API to power flag analytics charts
|
|
60
64
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
61
|
-
flags cannot be retrieved from the API or a non
|
|
65
|
+
flags cannot be retrieved from the API or a non-existent feature is
|
|
62
66
|
requested
|
|
63
67
|
@param data.logger: an instance of the pino Logger class to use for logging
|
|
64
|
-
|
|
65
|
-
|
|
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);
|
|
66
75
|
/**
|
|
67
76
|
* Get all the default for flags for the current environment.
|
|
68
77
|
*
|
package/build/sdk/index.js
CHANGED
|
@@ -105,6 +105,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
105
105
|
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
106
106
|
*
|
|
107
107
|
* @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
|
|
108
|
+
* Required unless offlineMode is True.
|
|
108
109
|
@param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
|
|
109
110
|
@param data.customHeaders: Additional headers to add to requests made to the
|
|
110
111
|
Flagsmith API
|
|
@@ -118,32 +119,54 @@ var Flagsmith = /** @class */ (function () {
|
|
|
118
119
|
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
119
120
|
API to power flag analytics charts
|
|
120
121
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
121
|
-
flags cannot be retrieved from the API or a non
|
|
122
|
+
flags cannot be retrieved from the API or a non-existent feature is
|
|
122
123
|
requested
|
|
123
124
|
@param data.logger: an instance of the pino Logger class to use for logging
|
|
124
|
-
|
|
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
|
+
*/
|
|
125
131
|
function Flagsmith(data) {
|
|
132
|
+
// if (!data.offlineMode && !data.environmentKey) {
|
|
133
|
+
// throw new Error('ValueError: environmentKey is required.');
|
|
134
|
+
// }
|
|
135
|
+
if (data === void 0) { data = {}; }
|
|
126
136
|
var _a;
|
|
127
|
-
this.
|
|
137
|
+
this.environmentKey = undefined;
|
|
138
|
+
this.apiUrl = undefined;
|
|
128
139
|
this.enableLocalEvaluation = false;
|
|
129
140
|
this.environmentRefreshIntervalSeconds = 60;
|
|
130
141
|
this.enableAnalytics = false;
|
|
142
|
+
this.offlineMode = false;
|
|
143
|
+
this.offlineHandler = undefined;
|
|
131
144
|
this.agent = data.agent;
|
|
132
145
|
this.environmentKey = data.environmentKey;
|
|
133
146
|
this.apiUrl = data.apiUrl || this.apiUrl;
|
|
134
147
|
this.customHeaders = data.customHeaders;
|
|
135
|
-
this.requestTimeoutMs =
|
|
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';
|
|
@@ -26,10 +27,9 @@ export { FlagsmithCache, FlagsmithConfig } from './types';
|
|
|
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
30
|
export class Flagsmith {
|
|
31
|
-
environmentKey?: string;
|
|
32
|
-
apiUrl
|
|
31
|
+
environmentKey?: string = undefined;
|
|
32
|
+
apiUrl?: string = undefined;
|
|
33
33
|
customHeaders?: { [key: string]: any };
|
|
34
34
|
agent: RequestInit['agent'];
|
|
35
35
|
requestTimeoutMs?: number;
|
|
@@ -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,16 +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.requestTimeoutMs =
|
|
102
|
+
this.requestTimeoutMs =
|
|
103
|
+
1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
|
|
91
104
|
this.enableLocalEvaluation = data.enableLocalEvaluation;
|
|
92
105
|
this.environmentRefreshIntervalSeconds =
|
|
93
106
|
data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds;
|
|
@@ -95,14 +108,26 @@ export class Flagsmith {
|
|
|
95
108
|
this.enableAnalytics = data.enableAnalytics || false;
|
|
96
109
|
this.defaultFlagHandler = data.defaultFlagHandler;
|
|
97
110
|
|
|
98
|
-
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
|
|
99
|
-
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
100
|
-
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
101
111
|
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
102
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
|
+
}
|
|
103
126
|
|
|
104
127
|
if (!!data.cache) {
|
|
105
|
-
const missingMethods: string[] = ['has', 'get', 'set'].filter(
|
|
128
|
+
const missingMethods: string[] = ['has', 'get', 'set'].filter(
|
|
129
|
+
method => data.cache && !data.cache[method]
|
|
130
|
+
);
|
|
106
131
|
|
|
107
132
|
if (missingMethods.length > 0) {
|
|
108
133
|
throw new Error(
|
|
@@ -114,28 +139,40 @@ export class Flagsmith {
|
|
|
114
139
|
this.cache = data.cache;
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
if (this.
|
|
118
|
-
if (!this.environmentKey
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
121
162
|
);
|
|
163
|
+
this.environmentDataPollingManager.start();
|
|
164
|
+
this.updateEnvironment();
|
|
122
165
|
}
|
|
123
|
-
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
124
|
-
this,
|
|
125
|
-
this.environmentRefreshIntervalSeconds
|
|
126
|
-
);
|
|
127
|
-
this.environmentDataPollingManager.start();
|
|
128
|
-
this.updateEnvironment();
|
|
129
|
-
}
|
|
130
166
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
176
|
}
|
|
140
177
|
/**
|
|
141
178
|
* Get all the default for flags for the current environment.
|
|
@@ -143,15 +180,15 @@ export class Flagsmith {
|
|
|
143
180
|
* @returns Flags object holding all the flags for the current environment.
|
|
144
181
|
*/
|
|
145
182
|
async getEnvironmentFlags(): Promise<Flags> {
|
|
146
|
-
const cachedItem = !!this.cache && await this.cache.get(`flags`);
|
|
183
|
+
const cachedItem = !!this.cache && (await this.cache.get(`flags`));
|
|
147
184
|
if (!!cachedItem) {
|
|
148
185
|
return cachedItem;
|
|
149
186
|
}
|
|
150
|
-
if (this.enableLocalEvaluation) {
|
|
187
|
+
if (this.enableLocalEvaluation && !this.offlineMode) {
|
|
151
188
|
return new Promise((resolve, reject) =>
|
|
152
189
|
this.environmentPromise!.then(() => {
|
|
153
190
|
resolve(this.getEnvironmentFlagsFromDocument());
|
|
154
|
-
}).catch(
|
|
191
|
+
}).catch(e => reject(e))
|
|
155
192
|
);
|
|
156
193
|
}
|
|
157
194
|
if (this.environment) {
|
|
@@ -160,6 +197,7 @@ export class Flagsmith {
|
|
|
160
197
|
|
|
161
198
|
return this.getEnvironmentFlagsFromApi();
|
|
162
199
|
}
|
|
200
|
+
|
|
163
201
|
/**
|
|
164
202
|
* Get all the flags for the current environment for a given identity. Will also
|
|
165
203
|
upsert all traits to the Flagsmith API for future evaluations. Providing a
|
|
@@ -173,10 +211,10 @@ export class Flagsmith {
|
|
|
173
211
|
*/
|
|
174
212
|
async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
|
|
175
213
|
if (!identifier) {
|
|
176
|
-
throw new Error(
|
|
214
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
177
215
|
}
|
|
178
216
|
|
|
179
|
-
const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
|
|
217
|
+
const cachedItem = !!this.cache && (await this.cache.get(`flags-${identifier}`));
|
|
180
218
|
if (!!cachedItem) {
|
|
181
219
|
return cachedItem;
|
|
182
220
|
}
|
|
@@ -188,6 +226,10 @@ export class Flagsmith {
|
|
|
188
226
|
}).catch(e => reject(e))
|
|
189
227
|
);
|
|
190
228
|
}
|
|
229
|
+
if (this.offlineMode) {
|
|
230
|
+
return this.getIdentityFlagsFromDocument(identifier, traits || {});
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
return this.getIdentityFlagsFromApi(identifier, traits);
|
|
192
234
|
}
|
|
193
235
|
|
|
@@ -207,7 +249,7 @@ export class Flagsmith {
|
|
|
207
249
|
traits?: { [key: string]: any }
|
|
208
250
|
): Promise<SegmentModel[]> {
|
|
209
251
|
if (!identifier) {
|
|
210
|
-
throw new Error(
|
|
252
|
+
throw new Error('`identifier` argument is missing or invalid.');
|
|
211
253
|
}
|
|
212
254
|
|
|
213
255
|
traits = traits || {};
|
|
@@ -224,7 +266,7 @@ export class Flagsmith {
|
|
|
224
266
|
|
|
225
267
|
const segments = getIdentitySegments(this.environment, identityModel);
|
|
226
268
|
return resolve(segments);
|
|
227
|
-
}).catch(
|
|
269
|
+
}).catch(e => reject(e));
|
|
228
270
|
});
|
|
229
271
|
}
|
|
230
272
|
console.error('This function is only permitted with local evaluation.');
|
|
@@ -286,7 +328,7 @@ export class Flagsmith {
|
|
|
286
328
|
headers: headers
|
|
287
329
|
},
|
|
288
330
|
this.retries,
|
|
289
|
-
this.requestTimeoutMs || undefined
|
|
331
|
+
this.requestTimeoutMs || undefined
|
|
290
332
|
);
|
|
291
333
|
|
|
292
334
|
if (data.status !== 200) {
|
|
@@ -304,6 +346,9 @@ export class Flagsmith {
|
|
|
304
346
|
private environmentPromise: Promise<any> | undefined;
|
|
305
347
|
|
|
306
348
|
private async getEnvironmentFromApi() {
|
|
349
|
+
if (!this.environmentUrl) {
|
|
350
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
351
|
+
}
|
|
307
352
|
const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
|
|
308
353
|
return buildEnvironmentModel(environment_data);
|
|
309
354
|
}
|
|
@@ -321,7 +366,10 @@ export class Flagsmith {
|
|
|
321
366
|
return flags;
|
|
322
367
|
}
|
|
323
368
|
|
|
324
|
-
private async getIdentityFlagsFromDocument(
|
|
369
|
+
private async getIdentityFlagsFromDocument(
|
|
370
|
+
identifier: string,
|
|
371
|
+
traits: { [key: string]: any }
|
|
372
|
+
): Promise<Flags> {
|
|
325
373
|
const identityModel = this.buildIdentityModel(
|
|
326
374
|
identifier,
|
|
327
375
|
Object.keys(traits).map(key => ({
|
|
@@ -348,6 +396,9 @@ export class Flagsmith {
|
|
|
348
396
|
}
|
|
349
397
|
|
|
350
398
|
private async getEnvironmentFlagsFromApi() {
|
|
399
|
+
if (!this.environmentFlagsUrl) {
|
|
400
|
+
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
401
|
+
}
|
|
351
402
|
try {
|
|
352
403
|
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
353
404
|
const flags = Flags.fromAPIFlags({
|
|
@@ -361,6 +412,9 @@ export class Flagsmith {
|
|
|
361
412
|
}
|
|
362
413
|
return flags;
|
|
363
414
|
} catch (e) {
|
|
415
|
+
if (this.offlineHandler) {
|
|
416
|
+
return this.getEnvironmentFlagsFromDocument();
|
|
417
|
+
}
|
|
364
418
|
if (this.defaultFlagHandler) {
|
|
365
419
|
return new Flags({
|
|
366
420
|
flags: {},
|
|
@@ -373,6 +427,9 @@ export class Flagsmith {
|
|
|
373
427
|
}
|
|
374
428
|
|
|
375
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
|
+
}
|
|
376
433
|
try {
|
|
377
434
|
const data = generateIdentitiesData(identifier, traits);
|
|
378
435
|
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
@@ -387,6 +444,9 @@ export class Flagsmith {
|
|
|
387
444
|
}
|
|
388
445
|
return flags;
|
|
389
446
|
} catch (e) {
|
|
447
|
+
if (this.offlineHandler) {
|
|
448
|
+
return this.getIdentityFlagsFromDocument(identifier, traits);
|
|
449
|
+
}
|
|
390
450
|
if (this.defaultFlagHandler) {
|
|
391
451
|
return new Flags({
|
|
392
452
|
flags: {},
|
|
@@ -405,4 +465,3 @@ export class Flagsmith {
|
|
|
405
465
|
}
|
|
406
466
|
|
|
407
467
|
export default Flagsmith;
|
|
408
|
-
|
|
@@ -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');
|
|
@@ -202,7 +203,7 @@ test('request timeout uses default if not provided', async () => {
|
|
|
202
203
|
expect(flg.requestTimeoutMs).toBe(10000);
|
|
203
204
|
})
|
|
204
205
|
|
|
205
|
-
test('
|
|
206
|
+
test('test_throws_when_no_identityFlags_returned_due_to_error', async () => {
|
|
206
207
|
// @ts-ignore
|
|
207
208
|
fetch.mockReturnValue(Promise.resolve(new Response('bad data')));
|
|
208
209
|
|
|
@@ -275,10 +276,108 @@ test('getIdentitySegments throws error if identifier is empty string', () => {
|
|
|
275
276
|
})
|
|
276
277
|
|
|
277
278
|
|
|
278
|
-
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) {
|
|
279
378
|
// TODO: this has been pulled out of tests above as a helper function.
|
|
280
379
|
// I'm not entirely sure why it's necessary, however, we should look to remove.
|
|
281
|
-
|
|
380
|
+
environmentModel.featureStates.forEach(fs => {
|
|
282
381
|
// @ts-ignore
|
|
283
382
|
fs.featurestateUUID = undefined;
|
|
284
383
|
fs.multivariateFeatureStateValues.forEach(mvfsv => {
|
|
@@ -286,7 +385,7 @@ async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|
|
|
286
385
|
mvfsv.mvFsValueUuid = undefined;
|
|
287
386
|
})
|
|
288
387
|
});
|
|
289
|
-
|
|
388
|
+
environmentModel.project.segments.forEach(s => {
|
|
290
389
|
s.featureStates.forEach(fs => {
|
|
291
390
|
// @ts-ignore
|
|
292
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) {
|