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.
@@ -1,5 +1,6 @@
1
- import { RequestInit } from "node-fetch";
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: string;
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: string;
27
- identitiesUrl: string;
28
- environmentUrl: string;
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 existent feature is
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
- constructor(data: FlagsmithConfig);
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
  *
@@ -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 existent feature is
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
- this.apiUrl = DEFAULT_API_URL;
126
- this.requestTimeoutSeconds = 10;
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.requestTimeoutSeconds = data.requestTimeoutSeconds;
135
- this.requestTimeoutMs = data.requestTimeoutSeconds ? data.requestTimeoutSeconds * 1000 : undefined;
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.enableLocalEvaluation) {
155
- if (!this.environmentKey.startsWith('ser.')) {
156
- console.error('In order to use local evaluation, please generate a server key in the environment settings page.');
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.environmentDataPollingManager = new polling_manager_1.EnvironmentDataPollingManager(this, this.environmentRefreshIntervalSeconds);
159
- this.environmentDataPollingManager.start();
160
- this.updateEnvironment();
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("`identifier` argument is missing or invalid.");
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("`identifier` argument is missing or invalid.");
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: return [4 /*yield*/, this.getJSONResponse(this.environmentUrl, 'GET')];
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
- _a.trys.push([0, 4, , 5]);
453
- return [4 /*yield*/, this.getJSONResponse(this.environmentFlagsUrl, 'GET')];
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*/, 3];
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 2:
509
+ case 3:
465
510
  // @ts-ignore node-cache types are incorrect, ttl should be optional
466
511
  _a.sent();
467
- _a.label = 3;
468
- case 3: return [2 /*return*/, flags];
469
- case 4:
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 5: return [2 /*return*/];
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
- _a.trys.push([0, 4, , 5]);
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 1:
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*/, 3];
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 2:
555
+ case 3:
503
556
  // @ts-ignore node-cache types are incorrect, ttl should be optional
504
557
  _a.sent();
505
- _a.label = 3;
506
- case 3: return [2 /*return*/, flags];
507
- case 4:
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 5: return [2 /*return*/];
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;
@@ -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: string;
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.1.0",
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 "node-fetch";
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 "pino";
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: string = DEFAULT_API_URL;
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: string;
44
- identitiesUrl: string;
45
- environmentUrl: string;
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 existent feature is
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
- constructor(data: FlagsmithConfig) {
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.requestTimeoutSeconds = data.requestTimeoutSeconds;
91
- this.requestTimeoutMs = data.requestTimeoutSeconds ? data.requestTimeoutSeconds * 1000 : undefined;
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(method => data.cache && !data.cache[method]);
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.enableLocalEvaluation) {
119
- if (!this.environmentKey.startsWith('ser.')) {
120
- console.error(
121
- 'In order to use local evaluation, please generate a server key in the environment settings page.'
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
- this.analyticsProcessor = data.enableAnalytics
133
- ? new AnalyticsProcessor({
134
- environmentKey: this.environmentKey,
135
- baseApiUrl: this.apiUrl,
136
- requestTimeoutMs: this.requestTimeoutMs,
137
- logger: this.logger
138
- })
139
- : undefined;
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((e) => reject(e))
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("`identifier` argument is missing or invalid.")
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("`identifier` argument is missing or invalid.")
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((e) => reject(e));
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(identifier: string, traits: { [key: string]: any }): Promise<Flags> {
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: string;
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.1,
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('test_throws_when_no_identity_flags_returned_due_to_error', async () => {
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 function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
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
- enviromentModel.featureStates.forEach(fs => {
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
- enviromentModel.project.segments.forEach(s => {
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
+ });
@@ -42,8 +42,8 @@ export function flagsmith(params = {}) {
42
42
  });
43
43
  }
44
44
 
45
- export function environmentJSON() {
46
- return readFileSync(DATA_DIR + 'environment.json', 'utf-8');
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) {