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.
@@ -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,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: string;
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: string;
26
- identitiesUrl: string;
27
- environmentUrl: string;
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 existent feature is
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
- 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);
66
75
  /**
67
76
  * Get all the default for flags for the current environment.
68
77
  *
@@ -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 existent feature is
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.apiUrl = DEFAULT_API_URL;
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 = 1000 * ((_a = data.requestTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_REQUEST_TIMEOUT_SECONDS);
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.1",
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';
@@ -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: string = DEFAULT_API_URL;
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: 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,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 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.requestTimeoutMs = 1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
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(method => data.cache && !data.cache[method]);
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.enableLocalEvaluation) {
118
- if (!this.environmentKey.startsWith('ser.')) {
119
- console.error(
120
- '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
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
- this.analyticsProcessor = data.enableAnalytics
132
- ? new AnalyticsProcessor({
133
- environmentKey: this.environmentKey,
134
- baseApiUrl: this.apiUrl,
135
- requestTimeoutMs: this.requestTimeoutMs,
136
- logger: this.logger
137
- })
138
- : 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
+ }
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((e) => reject(e))
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("`identifier` argument is missing or invalid.")
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("`identifier` argument is missing or invalid.")
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((e) => reject(e));
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(identifier: string, traits: { [key: string]: any }): Promise<Flags> {
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: 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');
@@ -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('test_throws_when_no_identity_flags_returned_due_to_error', async () => {
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 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) {
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
- enviromentModel.featureStates.forEach(fs => {
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
- enviromentModel.project.segments.forEach(s => {
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
+ });
@@ -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) {