flagsmith-nodejs 3.1.1 → 3.3.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.
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import { FeatureStateModel } from '../features/models';
2
+ import { IdentityModel } from '../identities/models';
2
3
  import { ProjectModel } from '../projects/models';
3
- import { IntegrationModel } from './integrations/models';
4
4
 
5
5
  export class EnvironmentAPIKeyModel {
6
6
  id: number;
@@ -37,10 +37,7 @@ export class EnvironmentModel {
37
37
  apiKey: string;
38
38
  project: ProjectModel;
39
39
  featureStates: FeatureStateModel[] = [];
40
- amplitude_config?: IntegrationModel;
41
- segment_config?: IntegrationModel;
42
- mixpanel_config?: IntegrationModel;
43
- heap_config?: IntegrationModel;
40
+ identityOverrides: IdentityModel[] = [];
44
41
 
45
42
  constructor(id: number, apiKey: string, project: ProjectModel) {
46
43
  this.id = id;
@@ -1,4 +1,5 @@
1
1
  import { buildFeatureStateModel } from '../features/util';
2
+ import { buildIdentityModel } from '../identities/util';
2
3
  import { buildProjectModel } from '../projects/util';
3
4
  import { EnvironmentAPIKeyModel, EnvironmentModel } from './models';
4
5
 
@@ -13,6 +14,11 @@ export function buildEnvironmentModel(environmentJSON: any) {
13
14
  project
14
15
  );
15
16
  environmentModel.featureStates = featureStates;
17
+ if (!!environmentJSON.identity_overrides) {
18
+ environmentModel.identityOverrides = environmentJSON.identity_overrides.map((identityData: any) =>
19
+ buildIdentityModel(identityData)
20
+ );
21
+ }
16
22
  return environmentModel;
17
23
  }
18
24
 
@@ -16,25 +16,26 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
16
16
  featuresStateModelJSON.enabled,
17
17
  featuresStateModelJSON.django_id,
18
18
  featuresStateModelJSON.feature_state_value,
19
- featuresStateModelJSON.uuid
19
+ featuresStateModelJSON.featurestate_uuid
20
20
  );
21
21
 
22
- featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
23
- buildFeatureSegment(featuresStateModelJSON.feature_segment) :
22
+ featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
23
+ buildFeatureSegment(featuresStateModelJSON.feature_segment) :
24
24
  undefined;
25
25
 
26
26
  const multivariateFeatureStateValues = featuresStateModelJSON.multivariate_feature_state_values
27
27
  ? featuresStateModelJSON.multivariate_feature_state_values.map((fsv: any) => {
28
- const featureOption = new MultivariateFeatureOptionModel(
29
- fsv.multivariate_feature_option.value,
30
- fsv.multivariate_feature_option.id
31
- );
32
- return new MultivariateFeatureStateValueModel(
33
- featureOption,
34
- fsv.percentage_allocation,
35
- fsv.id
36
- );
37
- })
28
+ const featureOption = new MultivariateFeatureOptionModel(
29
+ fsv.multivariate_feature_option.value,
30
+ fsv.multivariate_feature_option.id
31
+ );
32
+ return new MultivariateFeatureStateValueModel(
33
+ featureOption,
34
+ fsv.percentage_allocation,
35
+ fsv.id,
36
+ fsv.mv_fs_value_uuid
37
+ );
38
+ })
38
39
  : [];
39
40
 
40
41
  featureStateModel.multivariateFeatureStateValues = multivariateFeatureStateValues;
@@ -7,7 +7,6 @@ import { SegmentModel } from './segments/models';
7
7
  import { FeatureStateNotFound } from './utils/errors';
8
8
 
9
9
  export { EnvironmentModel } from './environments/models';
10
- export { IntegrationModel } from './environments/integrations/models';
11
10
  export { FeatureStateModel } from './features/models';
12
11
  export { IdentityModel } from './identities/models';
13
12
  export { TraitModel } from './identities/traits/models';
package/index.ts CHANGED
@@ -11,9 +11,12 @@ export {
11
11
  default
12
12
  } from './sdk';
13
13
 
14
+ export {
15
+ FlagsmithConfig
16
+ } from './sdk/types'
17
+
14
18
  export {
15
19
  EnvironmentModel,
16
- IntegrationModel,
17
20
  FeatureStateModel,
18
21
  IdentityModel,
19
22
  TraitModel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagsmith-nodejs",
3
- "version": "3.1.1",
3
+ "version": "3.3.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,16 @@ 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;
51
+
52
+ identitiesWithOverridesByIdentifier?: Map<string, IdentityModel>;
49
53
 
50
54
  private cache?: FlagsmithCache;
51
55
  private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
@@ -65,6 +69,7 @@ export class Flagsmith {
65
69
  * const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
66
70
  *
67
71
  * @param {string} data.environmentKey: The environment key obtained from Flagsmith interface
72
+ * Required unless offlineMode is True.
68
73
  @param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with
69
74
  @param data.customHeaders: Additional headers to add to requests made to the
70
75
  Flagsmith API
@@ -78,16 +83,26 @@ export class Flagsmith {
78
83
  @param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
79
84
  API to power flag analytics charts
80
85
  @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
86
+ flags cannot be retrieved from the API or a non-existent feature is
82
87
  requested
83
88
  @param data.logger: an instance of the pino Logger class to use for logging
84
- */
85
- constructor(data: FlagsmithConfig) {
89
+ @param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for
90
+ evaluating flags.
91
+ @param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment
92
+ document from another source when in offlineMode. Works in place of
93
+ defaultFlagHandler if offlineMode is not set and using remote evaluation.
94
+ */
95
+ constructor(data: FlagsmithConfig = {}) {
96
+ // if (!data.offlineMode && !data.environmentKey) {
97
+ // throw new Error('ValueError: environmentKey is required.');
98
+ // }
99
+
86
100
  this.agent = data.agent;
87
101
  this.environmentKey = data.environmentKey;
88
102
  this.apiUrl = data.apiUrl || this.apiUrl;
89
103
  this.customHeaders = data.customHeaders;
90
- this.requestTimeoutMs = 1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
104
+ this.requestTimeoutMs =
105
+ 1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
91
106
  this.enableLocalEvaluation = data.enableLocalEvaluation;
92
107
  this.environmentRefreshIntervalSeconds =
93
108
  data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds;
@@ -95,14 +110,26 @@ export class Flagsmith {
95
110
  this.enableAnalytics = data.enableAnalytics || false;
96
111
  this.defaultFlagHandler = data.defaultFlagHandler;
97
112
 
98
- this.environmentFlagsUrl = `${this.apiUrl}flags/`;
99
- this.identitiesUrl = `${this.apiUrl}identities/`;
100
- this.environmentUrl = `${this.apiUrl}environment-document/`;
101
113
  this.onEnvironmentChange = data.onEnvironmentChange;
102
114
  this.logger = data.logger || pino();
115
+ this.offlineMode = data.offlineMode || false;
116
+ this.offlineHandler = data.offlineHandler;
117
+
118
+ // argument validation
119
+ if (this.offlineMode && !this.offlineHandler) {
120
+ throw new Error('ValueError: offlineHandler must be provided to use offline mode.');
121
+ } else if (this.defaultFlagHandler && this.offlineHandler) {
122
+ throw new Error('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
123
+ }
124
+
125
+ if (this.offlineHandler) {
126
+ this.environment = this.offlineHandler.getEnvironment();
127
+ }
103
128
 
104
129
  if (!!data.cache) {
105
- const missingMethods: string[] = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
130
+ const missingMethods: string[] = ['has', 'get', 'set'].filter(
131
+ method => data.cache && !data.cache[method]
132
+ );
106
133
 
107
134
  if (missingMethods.length > 0) {
108
135
  throw new Error(
@@ -114,28 +141,40 @@ export class Flagsmith {
114
141
  this.cache = data.cache;
115
142
  }
116
143
 
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.'
144
+ if (!this.offlineMode) {
145
+ if (!this.environmentKey) {
146
+ throw new Error('ValueError: environmentKey is required.');
147
+ }
148
+
149
+ const apiUrl = data.apiUrl || DEFAULT_API_URL;
150
+ this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
151
+ this.environmentFlagsUrl = `${this.apiUrl}flags/`;
152
+ this.identitiesUrl = `${this.apiUrl}identities/`;
153
+ this.environmentUrl = `${this.apiUrl}environment-document/`;
154
+
155
+ if (this.enableLocalEvaluation) {
156
+ if (!this.environmentKey.startsWith('ser.')) {
157
+ console.error(
158
+ 'In order to use local evaluation, please generate a server key in the environment settings page.'
159
+ );
160
+ }
161
+ this.environmentDataPollingManager = new EnvironmentDataPollingManager(
162
+ this,
163
+ this.environmentRefreshIntervalSeconds
121
164
  );
165
+ this.environmentDataPollingManager.start();
166
+ this.updateEnvironment();
122
167
  }
123
- this.environmentDataPollingManager = new EnvironmentDataPollingManager(
124
- this,
125
- this.environmentRefreshIntervalSeconds
126
- );
127
- this.environmentDataPollingManager.start();
128
- this.updateEnvironment();
129
- }
130
168
 
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;
169
+ this.analyticsProcessor = data.enableAnalytics
170
+ ? new AnalyticsProcessor({
171
+ environmentKey: this.environmentKey,
172
+ baseApiUrl: this.apiUrl,
173
+ requestTimeoutMs: this.requestTimeoutMs,
174
+ logger: this.logger
175
+ })
176
+ : undefined;
177
+ }
139
178
  }
140
179
  /**
141
180
  * Get all the default for flags for the current environment.
@@ -143,15 +182,15 @@ export class Flagsmith {
143
182
  * @returns Flags object holding all the flags for the current environment.
144
183
  */
145
184
  async getEnvironmentFlags(): Promise<Flags> {
146
- const cachedItem = !!this.cache && await this.cache.get(`flags`);
185
+ const cachedItem = !!this.cache && (await this.cache.get(`flags`));
147
186
  if (!!cachedItem) {
148
187
  return cachedItem;
149
188
  }
150
- if (this.enableLocalEvaluation) {
189
+ if (this.enableLocalEvaluation && !this.offlineMode) {
151
190
  return new Promise((resolve, reject) =>
152
191
  this.environmentPromise!.then(() => {
153
192
  resolve(this.getEnvironmentFlagsFromDocument());
154
- }).catch((e) => reject(e))
193
+ }).catch(e => reject(e))
155
194
  );
156
195
  }
157
196
  if (this.environment) {
@@ -160,6 +199,7 @@ export class Flagsmith {
160
199
 
161
200
  return this.getEnvironmentFlagsFromApi();
162
201
  }
202
+
163
203
  /**
164
204
  * Get all the flags for the current environment for a given identity. Will also
165
205
  upsert all traits to the Flagsmith API for future evaluations. Providing a
@@ -173,10 +213,10 @@ export class Flagsmith {
173
213
  */
174
214
  async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
175
215
  if (!identifier) {
176
- throw new Error("`identifier` argument is missing or invalid.")
216
+ throw new Error('`identifier` argument is missing or invalid.');
177
217
  }
178
218
 
179
- const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
219
+ const cachedItem = !!this.cache && (await this.cache.get(`flags-${identifier}`));
180
220
  if (!!cachedItem) {
181
221
  return cachedItem;
182
222
  }
@@ -188,6 +228,10 @@ export class Flagsmith {
188
228
  }).catch(e => reject(e))
189
229
  );
190
230
  }
231
+ if (this.offlineMode) {
232
+ return this.getIdentityFlagsFromDocument(identifier, traits || {});
233
+ }
234
+
191
235
  return this.getIdentityFlagsFromApi(identifier, traits);
192
236
  }
193
237
 
@@ -207,14 +251,14 @@ export class Flagsmith {
207
251
  traits?: { [key: string]: any }
208
252
  ): Promise<SegmentModel[]> {
209
253
  if (!identifier) {
210
- throw new Error("`identifier` argument is missing or invalid.")
254
+ throw new Error('`identifier` argument is missing or invalid.');
211
255
  }
212
256
 
213
257
  traits = traits || {};
214
258
  if (this.enableLocalEvaluation) {
215
259
  return new Promise((resolve, reject) => {
216
260
  return this.environmentPromise!.then(() => {
217
- const identityModel = this.buildIdentityModel(
261
+ const identityModel = this.getIdentityModel(
218
262
  identifier,
219
263
  Object.keys(traits || {}).map(key => ({
220
264
  key,
@@ -224,7 +268,7 @@ export class Flagsmith {
224
268
 
225
269
  const segments = getIdentitySegments(this.environment, identityModel);
226
270
  return resolve(segments);
227
- }).catch((e) => reject(e));
271
+ }).catch(e => reject(e));
228
272
  });
229
273
  }
230
274
  console.error('This function is only permitted with local evaluation.');
@@ -247,6 +291,11 @@ export class Flagsmith {
247
291
  } else {
248
292
  this.environment = await request;
249
293
  }
294
+ if (this.environment.identityOverrides?.length) {
295
+ this.identitiesWithOverridesByIdentifier = new Map<string, IdentityModel>(
296
+ this.environment.identityOverrides.map(identity => [identity.identifier, identity]
297
+ ));
298
+ }
250
299
  if (this.onEnvironmentChange) {
251
300
  this.onEnvironmentChange(null, this.environment);
252
301
  }
@@ -286,7 +335,7 @@ export class Flagsmith {
286
335
  headers: headers
287
336
  },
288
337
  this.retries,
289
- this.requestTimeoutMs || undefined,
338
+ this.requestTimeoutMs || undefined
290
339
  );
291
340
 
292
341
  if (data.status !== 200) {
@@ -304,6 +353,9 @@ export class Flagsmith {
304
353
  private environmentPromise: Promise<any> | undefined;
305
354
 
306
355
  private async getEnvironmentFromApi() {
356
+ if (!this.environmentUrl) {
357
+ throw new Error('`apiUrl` argument is missing or invalid.');
358
+ }
307
359
  const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
308
360
  return buildEnvironmentModel(environment_data);
309
361
  }
@@ -321,8 +373,11 @@ export class Flagsmith {
321
373
  return flags;
322
374
  }
323
375
 
324
- private async getIdentityFlagsFromDocument(identifier: string, traits: { [key: string]: any }): Promise<Flags> {
325
- const identityModel = this.buildIdentityModel(
376
+ private async getIdentityFlagsFromDocument(
377
+ identifier: string,
378
+ traits: { [key: string]: any }
379
+ ): Promise<Flags> {
380
+ const identityModel = this.getIdentityModel(
326
381
  identifier,
327
382
  Object.keys(traits).map(key => ({
328
383
  key,
@@ -348,6 +403,9 @@ export class Flagsmith {
348
403
  }
349
404
 
350
405
  private async getEnvironmentFlagsFromApi() {
406
+ if (!this.environmentFlagsUrl) {
407
+ throw new Error('`apiUrl` argument is missing or invalid.');
408
+ }
351
409
  try {
352
410
  const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
353
411
  const flags = Flags.fromAPIFlags({
@@ -361,6 +419,9 @@ export class Flagsmith {
361
419
  }
362
420
  return flags;
363
421
  } catch (e) {
422
+ if (this.offlineHandler) {
423
+ return this.getEnvironmentFlagsFromDocument();
424
+ }
364
425
  if (this.defaultFlagHandler) {
365
426
  return new Flags({
366
427
  flags: {},
@@ -373,6 +434,9 @@ export class Flagsmith {
373
434
  }
374
435
 
375
436
  private async getIdentityFlagsFromApi(identifier: string, traits: { [key: string]: any }) {
437
+ if (!this.identitiesUrl) {
438
+ throw new Error('`apiUrl` argument is missing or invalid.');
439
+ }
376
440
  try {
377
441
  const data = generateIdentitiesData(identifier, traits);
378
442
  const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
@@ -387,6 +451,9 @@ export class Flagsmith {
387
451
  }
388
452
  return flags;
389
453
  } catch (e) {
454
+ if (this.offlineHandler) {
455
+ return this.getIdentityFlagsFromDocument(identifier, traits);
456
+ }
390
457
  if (this.defaultFlagHandler) {
391
458
  return new Flags({
392
459
  flags: {},
@@ -398,11 +465,15 @@ export class Flagsmith {
398
465
  }
399
466
  }
400
467
 
401
- private buildIdentityModel(identifier: string, traits: { key: string; value: any }[]) {
468
+ private getIdentityModel(identifier: string, traits: { key: string; value: any }[]) {
402
469
  const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value));
470
+ let identityWithOverrides = this.identitiesWithOverridesByIdentifier?.get(identifier);
471
+ if (identityWithOverrides) {
472
+ identityWithOverrides.updateTraits(traitModels);
473
+ return identityWithOverrides;
474
+ }
403
475
  return new IdentityModel('0', traitModels, [], this.environment.apiKey, identifier);
404
476
  }
405
477
  }
406
478
 
407
479
  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
  }
@@ -17,6 +17,7 @@
17
17
  "feature_states": [
18
18
  {
19
19
  "feature_state_value": "segment_override",
20
+ "featurestate_uuid": "dd77a1ab-08cf-4743-8a3b-19e730444a14",
20
21
  "multivariate_feature_state_values": [],
21
22
  "django_id": 81027,
22
23
  "feature": {
@@ -88,5 +89,30 @@
88
89
  "featurestate_uuid": "96fc3503-09d7-48f1-a83b-2dc903d5c08a",
89
90
  "enabled": false
90
91
  }
92
+ ],
93
+ "identity_overrides": [
94
+ {
95
+ "identifier": "overridden-id",
96
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
97
+ "created_date": "2019-08-27T14:53:45.698555Z",
98
+ "updated_at": "2023-07-14 16:12:00.000000",
99
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
100
+ "identity_features": [
101
+ {
102
+ "id": 1,
103
+ "feature": {
104
+ "id": 1,
105
+ "name": "some_feature",
106
+ "type": "STANDARD"
107
+ },
108
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
109
+ "feature_state_value": "some-overridden-value",
110
+ "enabled": false,
111
+ "environment": 1,
112
+ "identity": null,
113
+ "feature_segment": null
114
+ }
115
+ ]
116
+ }
91
117
  ]
92
- }
118
+ }