flagsmith-nodejs 6.0.1 → 6.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.
Files changed (79) hide show
  1. package/.github/workflows/conventional-commit.yml +29 -0
  2. package/.github/workflows/pull_request.yaml +1 -1
  3. package/.github/workflows/release-please.yml +18 -0
  4. package/.gitmodules +1 -0
  5. package/.husky/pre-commit +0 -0
  6. package/.prettierignore +2 -1
  7. package/.prettierrc.cjs +9 -1
  8. package/.release-please-manifest.json +1 -0
  9. package/CHANGELOG.md +552 -0
  10. package/CODEOWNERS +1 -0
  11. package/README.md +2 -3
  12. package/build/cjs/flagsmith-engine/features/util.js +3 -3
  13. package/build/cjs/flagsmith-engine/index.d.ts +1 -1
  14. package/build/cjs/flagsmith-engine/index.js +2 -1
  15. package/build/cjs/flagsmith-engine/segments/models.js +7 -7
  16. package/build/cjs/flagsmith-engine/utils/hashing/index.js +1 -1
  17. package/build/cjs/index.d.ts +4 -4
  18. package/build/cjs/index.js +3 -1
  19. package/build/cjs/sdk/analytics.d.ts +1 -1
  20. package/build/cjs/sdk/analytics.js +3 -1
  21. package/build/cjs/sdk/index.d.ts +5 -5
  22. package/build/cjs/sdk/index.js +48 -10
  23. package/build/cjs/sdk/models.d.ts +25 -0
  24. package/build/cjs/sdk/models.js +25 -0
  25. package/build/cjs/sdk/types.d.ts +14 -4
  26. package/build/cjs/sdk/utils.d.ts +5 -4
  27. package/build/cjs/sdk/utils.js +16 -3
  28. package/build/esm/flagsmith-engine/features/models.js +1 -1
  29. package/build/esm/flagsmith-engine/features/util.js +3 -3
  30. package/build/esm/flagsmith-engine/index.d.ts +1 -1
  31. package/build/esm/flagsmith-engine/index.js +1 -1
  32. package/build/esm/flagsmith-engine/segments/models.js +7 -7
  33. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  34. package/build/esm/flagsmith-engine/utils/index.js +1 -1
  35. package/build/esm/index.d.ts +4 -4
  36. package/build/esm/index.js +3 -3
  37. package/build/esm/sdk/analytics.d.ts +1 -1
  38. package/build/esm/sdk/analytics.js +3 -1
  39. package/build/esm/sdk/index.d.ts +5 -5
  40. package/build/esm/sdk/index.js +48 -11
  41. package/build/esm/sdk/models.d.ts +25 -0
  42. package/build/esm/sdk/models.js +25 -0
  43. package/build/esm/sdk/types.d.ts +14 -4
  44. package/build/esm/sdk/utils.d.ts +5 -4
  45. package/build/esm/sdk/utils.js +14 -2
  46. package/flagsmith-engine/environments/util.ts +2 -2
  47. package/flagsmith-engine/features/models.ts +1 -1
  48. package/flagsmith-engine/features/util.ts +14 -14
  49. package/flagsmith-engine/identities/models.ts +1 -1
  50. package/flagsmith-engine/index.ts +1 -1
  51. package/flagsmith-engine/segments/evaluators.ts +2 -3
  52. package/flagsmith-engine/segments/models.ts +25 -15
  53. package/flagsmith-engine/utils/hashing/index.ts +3 -3
  54. package/flagsmith-engine/utils/index.ts +4 -2
  55. package/index.ts +19 -22
  56. package/package.json +1 -1
  57. package/release-please-config.json +62 -0
  58. package/sdk/analytics.ts +10 -6
  59. package/sdk/index.ts +91 -28
  60. package/sdk/models.ts +25 -0
  61. package/sdk/offline_handlers.ts +1 -1
  62. package/sdk/types.ts +17 -8
  63. package/sdk/utils.ts +21 -8
  64. package/tests/engine/e2e/engine.test.ts +2 -4
  65. package/tests/engine/unit/engine.test.ts +1 -6
  66. package/tests/engine/unit/features/models.test.ts +2 -2
  67. package/tests/engine/unit/identities/identities_builders.test.ts +1 -1
  68. package/tests/engine/unit/segments/segment_evaluators.test.ts +52 -23
  69. package/tests/engine/unit/segments/segments_model.test.ts +35 -37
  70. package/tests/engine/unit/utils/utils.test.ts +28 -30
  71. package/tests/sdk/analytics.test.ts +30 -26
  72. package/tests/sdk/flagsmith-cache.test.ts +84 -76
  73. package/tests/sdk/flagsmith-environment-flags.test.ts +121 -93
  74. package/tests/sdk/flagsmith-identity-flags.test.ts +155 -149
  75. package/tests/sdk/flagsmith.test.ts +202 -43
  76. package/tests/sdk/offline-handlers.test.ts +32 -32
  77. package/tests/sdk/polling.test.ts +0 -1
  78. package/tests/sdk/utils.ts +26 -18
  79. package/vitest.config.ts +10 -15
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash } from 'node:crypto';
2
2
  const md5 = (data) => createHash('md5').update(data).digest('hex');
3
3
  const makeRepeated = (arr, repeats) => Array.from({ length: repeats }, () => arr).flat();
4
4
  // https://stackoverflow.com/questions/12532871/how-to-convert-a-very-large-hex-number-to-decimal-in-javascript
@@ -14,7 +14,7 @@ export function getHashedPercentateForObjIds(objectIds, iterations = 1) {
14
14
  let toHash = makeRepeated(objectIds, iterations).join(',');
15
15
  const hashedValue = md5(toHash);
16
16
  const hashedInt = BigInt('0x' + hashedValue);
17
- const value = (Number((hashedInt % 9999n)) / 9998.0) * 100;
17
+ const value = (Number(hashedInt % 9999n) / 9998.0) * 100;
18
18
  // we ignore this for it's nearly impossible use case to catch
19
19
  /* istanbul ignore next */
20
20
  if (value === 100) {
@@ -1,4 +1,4 @@
1
- import { removeSemverSuffix } from "../segments/util.js";
1
+ import { removeSemverSuffix } from '../segments/util.js';
2
2
  export function getCastingFunction(traitType) {
3
3
  switch (traitType) {
4
4
  case 'boolean':
@@ -1,4 +1,4 @@
1
- export { AnalyticsProcessor, AnalyticsProcessorOptions, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, DefaultFlag, Flags, Flagsmith, } from './sdk/index.js';
2
- export { BaseOfflineHandler, LocalFileHandler, } from './sdk/offline_handlers.js';
3
- export { FlagsmithConfig } from './sdk/types.js';
4
- export { EnvironmentModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
1
+ export { AnalyticsProcessor, AnalyticsProcessorOptions, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, FlagsmithCache, BaseFlag, DefaultFlag, Flags, Flagsmith } from './sdk/index.js';
2
+ export { BaseOfflineHandler, LocalFileHandler } from './sdk/offline_handlers.js';
3
+ export { FlagsmithConfig, FlagsmithValue, TraitConfig } from './sdk/types.js';
4
+ export { EnvironmentModel, FeatureModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
@@ -1,3 +1,3 @@
1
- export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, DefaultFlag, Flags, Flagsmith, } from './sdk/index.js';
2
- export { BaseOfflineHandler, LocalFileHandler, } from './sdk/offline_handlers.js';
3
- export { EnvironmentModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
1
+ export { AnalyticsProcessor, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, BaseFlag, DefaultFlag, Flags, Flagsmith } from './sdk/index.js';
2
+ export { BaseOfflineHandler, LocalFileHandler } from './sdk/offline_handlers.js';
3
+ export { EnvironmentModel, FeatureModel, FeatureStateModel, IdentityModel, TraitModel, SegmentModel, OrganisationModel } from './flagsmith-engine/index.js';
@@ -1,5 +1,5 @@
1
1
  import { Logger } from 'pino';
2
- import { Fetch } from "./types.js";
2
+ import { Fetch } from './types.js';
3
3
  export declare const ANALYTICS_ENDPOINT = "./analytics/flags/";
4
4
  export interface AnalyticsProcessorOptions {
5
5
  /** URL of the Flagsmith analytics events API endpoint
@@ -1,4 +1,5 @@
1
1
  import { pino } from 'pino';
2
+ import { getUserAgent } from './utils.js';
2
3
  export const ANALYTICS_ENDPOINT = './analytics/flags/';
3
4
  /** Duration in seconds to wait before trying to flush collected data after {@link trackFeature} is called. **/
4
5
  const ANALYTICS_TIMER = 10;
@@ -44,7 +45,8 @@ export class AnalyticsProcessor {
44
45
  signal: AbortSignal.timeout(this.requestTimeoutMs),
45
46
  headers: {
46
47
  'Content-Type': 'application/json',
47
- 'X-Environment-Key': this.environmentKey
48
+ 'X-Environment-Key': this.environmentKey,
49
+ 'User-Agent': getUserAgent()
48
50
  }
49
51
  });
50
52
  await this.currentFlush;
@@ -5,10 +5,10 @@ import { BaseOfflineHandler } from './offline_handlers.js';
5
5
  import { DefaultFlag, Flags } from './models.js';
6
6
  import { EnvironmentDataPollingManager } from './polling_manager.js';
7
7
  import { SegmentModel } from '../flagsmith-engine/index.js';
8
- import { FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types.js';
8
+ import { FlagsmithConfig, FlagsmithTraitValue, TraitConfig } from './types.js';
9
9
  export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js';
10
10
  export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
11
- export { DefaultFlag, Flags } from './models.js';
11
+ export { BaseFlag, DefaultFlag, Flags } from './models.js';
12
12
  export { EnvironmentDataPollingManager } from './polling_manager.js';
13
13
  export { FlagsmithCache, FlagsmithConfig } from './types.js';
14
14
  /**
@@ -34,7 +34,7 @@ export { FlagsmithCache, FlagsmithConfig } from './types.js';
34
34
  * const bannerVariation: string = identityFlags.getFeatureValue('banner_flag')
35
35
  *
36
36
  * @see FlagsmithConfig
37
- */
37
+ */
38
38
  export declare class Flagsmith {
39
39
  environmentKey?: string;
40
40
  apiUrl?: string;
@@ -84,12 +84,12 @@ export declare class Flagsmith {
84
84
  *
85
85
  * @param {string} identifier a unique identifier for the identity in the current
86
86
  environment, e.g. email address, username, uuid
87
- * @param {{[key:string]:any | ITraitConfig}} traits? a dictionary of traits to add / update on the identity in
87
+ * @param {{[key:string]:any | TraitConfig}} traits? a dictionary of traits to add / update on the identity in
88
88
  Flagsmith, e.g. {"num_orders": 10} or {age: {value: 30, transient: true}}
89
89
  * @returns Flags object holding all the flags for the given identity.
90
90
  */
91
91
  getIdentityFlags(identifier: string, traits?: {
92
- [key: string]: FlagsmithTraitValue | ITraitConfig;
92
+ [key: string]: FlagsmithTraitValue | TraitConfig;
93
93
  }, transient?: boolean): Promise<Flags>;
94
94
  /**
95
95
  * Get the segments for the current environment for a given identity. Will also
@@ -6,12 +6,12 @@ import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js';
6
6
  import { FlagsmithAPIError } from './errors.js';
7
7
  import { Flags } from './models.js';
8
8
  import { EnvironmentDataPollingManager } from './polling_manager.js';
9
- import { Deferred, generateIdentitiesData, retryFetch } from './utils.js';
9
+ import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
10
10
  import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js';
11
11
  import { pino } from 'pino';
12
12
  export { AnalyticsProcessor } from './analytics.js';
13
13
  export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
14
- export { DefaultFlag, Flags } from './models.js';
14
+ export { BaseFlag, DefaultFlag, Flags } from './models.js';
15
15
  export { EnvironmentDataPollingManager } from './polling_manager.js';
16
16
  const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
17
17
  const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
@@ -38,7 +38,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
38
38
  * const bannerVariation: string = identityFlags.getFeatureValue('banner_flag')
39
39
  *
40
40
  * @see FlagsmithConfig
41
- */
41
+ */
42
42
  export class Flagsmith {
43
43
  environmentKey = undefined;
44
44
  apiUrl = undefined;
@@ -107,7 +107,8 @@ export class Flagsmith {
107
107
  }
108
108
  const apiUrl = data.apiUrl || DEFAULT_API_URL;
109
109
  this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
110
- this.analyticsUrl = this.analyticsUrl || new URL(ANALYTICS_ENDPOINT, new Request(this.apiUrl).url).href;
110
+ this.analyticsUrl =
111
+ this.analyticsUrl || new URL(ANALYTICS_ENDPOINT, new Request(this.apiUrl).url).href;
111
112
  this.environmentFlagsUrl = `${this.apiUrl}flags/`;
112
113
  this.identitiesUrl = `${this.apiUrl}identities/`;
113
114
  this.environmentUrl = `${this.apiUrl}environment-document/`;
@@ -125,7 +126,7 @@ export class Flagsmith {
125
126
  environmentKey: this.environmentKey,
126
127
  analyticsUrl: this.analyticsUrl,
127
128
  requestTimeoutMs: this.requestTimeoutMs,
128
- logger: this.logger,
129
+ logger: this.logger
129
130
  });
130
131
  }
131
132
  }
@@ -164,7 +165,7 @@ export class Flagsmith {
164
165
  *
165
166
  * @param {string} identifier a unique identifier for the identity in the current
166
167
  environment, e.g. email address, username, uuid
167
- * @param {{[key:string]:any | ITraitConfig}} traits? a dictionary of traits to add / update on the identity in
168
+ * @param {{[key:string]:any | TraitConfig}} traits? a dictionary of traits to add / update on the identity in
168
169
  Flagsmith, e.g. {"num_orders": 10} or {age: {value: 30, transient: true}}
169
170
  * @returns Flags object holding all the flags for the given identity.
170
171
  */
@@ -268,6 +269,7 @@ export class Flagsmith {
268
269
  if (this.environmentKey) {
269
270
  headers['X-Environment-Key'] = this.environmentKey;
270
271
  }
272
+ headers['User-Agent'] = getUserAgent();
271
273
  if (this.customHeaders) {
272
274
  for (const [k, v] of Object.entries(this.customHeaders)) {
273
275
  headers[k] = v;
@@ -282,7 +284,7 @@ export class Flagsmith {
282
284
  if (data.status !== 200) {
283
285
  throw new FlagsmithAPIError(`Invalid request made to Flagsmith API. Response status code: ${data.status}`);
284
286
  }
285
- return data.json();
287
+ return { response: data, data: await data.json() };
286
288
  }
287
289
  /**
288
290
  * This promise ensures that the environment is retrieved before attempting to locally evaluate.
@@ -309,8 +311,43 @@ export class Flagsmith {
309
311
  if (!this.environmentUrl) {
310
312
  throw new Error('`apiUrl` argument is missing or invalid.');
311
313
  }
312
- const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
313
- return buildEnvironmentModel(environment_data);
314
+ const startTime = Date.now();
315
+ const documents = [];
316
+ let url = this.environmentUrl;
317
+ let loggedWarning = false;
318
+ while (true) {
319
+ try {
320
+ if (!loggedWarning) {
321
+ const elapsedMs = Date.now() - startTime;
322
+ if (elapsedMs > this.environmentRefreshIntervalSeconds * 1000) {
323
+ this.logger.warn(`Environment document retrieval exceeded the polling interval of ${this.environmentRefreshIntervalSeconds} seconds.`);
324
+ loggedWarning = true;
325
+ }
326
+ }
327
+ const { response, data } = await this.getJSONResponse(url, 'GET');
328
+ documents.push(data);
329
+ const linkHeader = response.headers.get('link');
330
+ if (linkHeader) {
331
+ const nextMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
332
+ if (nextMatch) {
333
+ const relativeUrl = decodeURIComponent(nextMatch[1]);
334
+ url = new URL(relativeUrl, this.apiUrl).href;
335
+ continue;
336
+ }
337
+ }
338
+ break;
339
+ }
340
+ catch (error) {
341
+ throw error;
342
+ }
343
+ }
344
+ // Compile the document
345
+ const compiledDocument = documents[0];
346
+ for (let i = 1; i < documents.length; i++) {
347
+ compiledDocument.identity_overrides = compiledDocument.identity_overrides || [];
348
+ compiledDocument.identity_overrides.push(...(documents[i].identity_overrides || []));
349
+ }
350
+ return buildEnvironmentModel(compiledDocument);
314
351
  }
315
352
  async getEnvironmentFlagsFromDocument() {
316
353
  const environment = await this.getEnvironment();
@@ -346,7 +383,7 @@ export class Flagsmith {
346
383
  if (!this.environmentFlagsUrl) {
347
384
  throw new Error('`apiUrl` argument is missing or invalid.');
348
385
  }
349
- const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
386
+ const { data: apiFlags } = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
350
387
  const flags = Flags.fromAPIFlags({
351
388
  apiFlags: apiFlags,
352
389
  analyticsProcessor: this.analyticsProcessor,
@@ -362,7 +399,7 @@ export class Flagsmith {
362
399
  throw new Error('`apiUrl` argument is missing or invalid.');
363
400
  }
364
401
  const data = generateIdentitiesData(identifier, traits, transient);
365
- const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
402
+ const { data: jsonResponse } = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
366
403
  const flags = Flags.fromAPIFlags({
367
404
  apiFlags: jsonResponse['flags'],
368
405
  analyticsProcessor: this.analyticsProcessor,
@@ -1,17 +1,42 @@
1
1
  import { FeatureStateModel } from '../flagsmith-engine/features/models.js';
2
2
  import { AnalyticsProcessor } from './analytics.js';
3
3
  type FlagValue = string | number | boolean | undefined;
4
+ /**
5
+ * A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}.
6
+ */
4
7
  export declare class BaseFlag {
8
+ /**
9
+ * Indicates whether this feature is enabled.
10
+ */
5
11
  enabled: boolean;
12
+ /**
13
+ * An optional {@link FlagValue} for this feature.
14
+ */
6
15
  value: FlagValue;
16
+ /**
17
+ * If true, the state for this feature was determined by a default flag handler. See {@link DefaultFlag}.
18
+ */
7
19
  isDefault: boolean;
8
20
  constructor(value: FlagValue, enabled: boolean, isDefault: boolean);
9
21
  }
22
+ /**
23
+ * A {@link BaseFlag} returned by a default flag handler when flag evaluation fails.
24
+ * @see FlagsmithConfig#defaultFlagHandler
25
+ */
10
26
  export declare class DefaultFlag extends BaseFlag {
11
27
  constructor(value: FlagValue, enabled: boolean);
12
28
  }
29
+ /**
30
+ * A Flagsmith feature retrieved from a successful flag evaluation.
31
+ */
13
32
  export declare class Flag extends BaseFlag {
33
+ /**
34
+ * An identifier for this feature, unique in a single Flagsmith installation.
35
+ */
14
36
  featureId: number;
37
+ /**
38
+ * The programmatic name for this feature, unique per Flagsmith project.
39
+ */
15
40
  featureName: string;
16
41
  constructor(params: {
17
42
  value: FlagValue;
@@ -1,6 +1,18 @@
1
+ /**
2
+ * A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}.
3
+ */
1
4
  export class BaseFlag {
5
+ /**
6
+ * Indicates whether this feature is enabled.
7
+ */
2
8
  enabled;
9
+ /**
10
+ * An optional {@link FlagValue} for this feature.
11
+ */
3
12
  value;
13
+ /**
14
+ * If true, the state for this feature was determined by a default flag handler. See {@link DefaultFlag}.
15
+ */
4
16
  isDefault;
5
17
  constructor(value, enabled, isDefault) {
6
18
  this.value = value;
@@ -8,13 +20,26 @@ export class BaseFlag {
8
20
  this.isDefault = isDefault;
9
21
  }
10
22
  }
23
+ /**
24
+ * A {@link BaseFlag} returned by a default flag handler when flag evaluation fails.
25
+ * @see FlagsmithConfig#defaultFlagHandler
26
+ */
11
27
  export class DefaultFlag extends BaseFlag {
12
28
  constructor(value, enabled) {
13
29
  super(value, enabled, true);
14
30
  }
15
31
  }
32
+ /**
33
+ * A Flagsmith feature retrieved from a successful flag evaluation.
34
+ */
16
35
  export class Flag extends BaseFlag {
36
+ /**
37
+ * An identifier for this feature, unique in a single Flagsmith installation.
38
+ */
17
39
  featureId;
40
+ /**
41
+ * The programmatic name for this feature, unique per Flagsmith project.
42
+ */
18
43
  featureName;
19
44
  constructor(params) {
20
45
  super(params.value, params.enabled, !!params.isDefault);
@@ -3,7 +3,10 @@ import { EnvironmentModel } from '../flagsmith-engine/index.js';
3
3
  import { Dispatcher } from 'undici-types';
4
4
  import { Logger } from 'pino';
5
5
  import { BaseOfflineHandler } from './offline_handlers.js';
6
- export type IFlagsmithValue<T = string | number | boolean | null> = T;
6
+ /**
7
+ * A concrete type for the possible values of a feature.
8
+ */
9
+ export type FlagsmithValue<T = string | number | boolean | null> = T;
7
10
  /**
8
11
  * Stores and retrieves {@link Flags} from a cache.
9
12
  */
@@ -89,7 +92,7 @@ export interface FlagsmithConfig {
89
92
  * const defaultHandler = () => new DefaultFlag(undefined, false)
90
93
  *
91
94
  * // Enable only VIP flags by default
92
- * const vipDefaultHandler = (key: string) => new Default(undefined, key.startsWith('vip_'))
95
+ * const vipDefaultHandler = (key: string) => new DefaultFlag(undefined, key.startsWith('vip_'))
93
96
  */
94
97
  defaultFlagHandler?: (flagKey: string) => DefaultFlag;
95
98
  cache?: FlagsmithCache;
@@ -109,8 +112,15 @@ export interface FlagsmithConfig {
109
112
  */
110
113
  offlineHandler?: BaseOfflineHandler;
111
114
  }
112
- export interface ITraitConfig {
115
+ /**
116
+ * Represents the configuration for a trait in Flagsmith.
117
+ *
118
+ * @property value The {@link FlagsmithTraitValue} for this trait.
119
+ * @property [transient] Indicates whether the trait should be persisted when used in a remote flag evaluation context.
120
+ * Defaults to false.
121
+ */
122
+ export interface TraitConfig {
113
123
  value: FlagsmithTraitValue;
114
124
  transient?: boolean;
115
125
  }
116
- export declare type FlagsmithTraitValue = IFlagsmithValue;
126
+ export declare type FlagsmithTraitValue = FlagsmithValue;
@@ -1,9 +1,9 @@
1
- import { Fetch, FlagsmithTraitValue, ITraitConfig } from './types.js';
2
- import { Dispatcher } from "undici-types";
1
+ import { Fetch, FlagsmithTraitValue, TraitConfig } from './types.js';
2
+ import { Dispatcher } from 'undici-types';
3
3
  type Traits = {
4
- [key: string]: ITraitConfig | FlagsmithTraitValue;
4
+ [key: string]: TraitConfig | FlagsmithTraitValue;
5
5
  };
6
- export declare function isTraitConfig(traitValue: ITraitConfig | FlagsmithTraitValue): traitValue is ITraitConfig;
6
+ export declare function isTraitConfig(traitValue: TraitConfig | FlagsmithTraitValue): traitValue is TraitConfig;
7
7
  export declare function generateIdentitiesData(identifier: string, traits: Traits, transient: boolean): {
8
8
  identifier: string;
9
9
  traits: ({
@@ -56,4 +56,5 @@ export declare class Deferred<T> {
56
56
  resolve(value: T | PromiseLike<T>): void;
57
57
  reject(reason?: unknown): void;
58
58
  }
59
+ export declare function getUserAgent(): string;
59
60
  export {};
@@ -1,3 +1,5 @@
1
+ const FLAGSMITH_USER_AGENT = 'flagsmith-nodejs-sdk';
2
+ const FLAGSMITH_UNKNOWN_VERSION = 'unknown';
1
3
  export function isTraitConfig(traitValue) {
2
4
  return !!traitValue && typeof traitValue == 'object' && traitValue.value !== undefined;
3
5
  }
@@ -7,13 +9,13 @@ export function generateIdentitiesData(identifier, traits, transient) {
7
9
  return {
8
10
  trait_key: key,
9
11
  trait_value: value?.value,
10
- transient: value?.transient,
12
+ transient: value?.transient
11
13
  };
12
14
  }
13
15
  else {
14
16
  return {
15
17
  trait_key: key,
16
- trait_value: value,
18
+ trait_value: value
17
19
  };
18
20
  }
19
21
  });
@@ -85,3 +87,13 @@ export class Deferred {
85
87
  this.rejectPromise(reason);
86
88
  }
87
89
  }
90
+ export function getUserAgent() {
91
+ try {
92
+ const packageJson = require('../package.json');
93
+ const version = packageJson?.version;
94
+ return version ? `${FLAGSMITH_USER_AGENT}/${version}` : FLAGSMITH_UNKNOWN_VERSION;
95
+ }
96
+ catch {
97
+ return FLAGSMITH_UNKNOWN_VERSION;
98
+ }
99
+ }
@@ -15,8 +15,8 @@ export function buildEnvironmentModel(environmentJSON: any) {
15
15
  );
16
16
  environmentModel.featureStates = featureStates;
17
17
  if (!!environmentJSON.identity_overrides) {
18
- environmentModel.identityOverrides = environmentJSON.identity_overrides.map((identityData: any) =>
19
- buildIdentityModel(identityData)
18
+ environmentModel.identityOverrides = environmentJSON.identity_overrides.map(
19
+ (identityData: any) => buildIdentityModel(identityData)
20
20
  );
21
21
  }
22
22
  return environmentModel;
@@ -1,4 +1,4 @@
1
- import { randomUUID as uuidv4 } from "node:crypto";
1
+ import { randomUUID as uuidv4 } from 'node:crypto';
2
2
  import { getHashedPercentateForObjIds } from '../utils/hashing/index.js';
3
3
 
4
4
  export class FeatureModel {
@@ -19,23 +19,23 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
19
19
  featuresStateModelJSON.featurestate_uuid
20
20
  );
21
21
 
22
- featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
23
- buildFeatureSegment(featuresStateModelJSON.feature_segment) :
24
- undefined;
22
+ featureStateModel.featureSegment = featuresStateModelJSON.feature_segment
23
+ ? buildFeatureSegment(featuresStateModelJSON.feature_segment)
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
- fsv.mv_fs_value_uuid
37
- );
38
- })
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
+ })
39
39
  : [];
40
40
 
41
41
  featureStateModel.multivariateFeatureStateValues = multivariateFeatureStateValues;
@@ -19,7 +19,7 @@ export class IdentityModel {
19
19
  environmentApiKey: string,
20
20
  identifier: string,
21
21
  identityUuid?: string,
22
- djangoID?: number,
22
+ djangoID?: number
23
23
  ) {
24
24
  this.identityUuid = identityUuid || uuidv4();
25
25
  this.createdDate = Date.parse(created_date) || Date.now();
@@ -7,7 +7,7 @@ import { SegmentModel } from './segments/models.js';
7
7
  import { FeatureStateNotFound } from './utils/errors.js';
8
8
 
9
9
  export { EnvironmentModel } from './environments/models.js';
10
- export { FeatureStateModel } from './features/models.js';
10
+ export { FeatureModel, FeatureStateModel } from './features/models.js';
11
11
  export { IdentityModel } from './identities/models.js';
12
12
  export { TraitModel } from './identities/traits/models.js';
13
13
  export { SegmentModel } from './segments/models.js';
@@ -67,11 +67,10 @@ export function traitsMatchSegmentCondition(
67
67
  }
68
68
  const traits = identityTraits.filter(t => t.traitKey === condition.property_);
69
69
  const trait = traits.length > 0 ? traits[0] : undefined;
70
- if (condition.operator === IS_SET ) {
70
+ if (condition.operator === IS_SET) {
71
71
  return !!trait;
72
- } else if (condition.operator === IS_NOT_SET){
72
+ } else if (condition.operator === IS_NOT_SET) {
73
73
  return trait == undefined;
74
74
  }
75
75
  return trait ? condition.matchesTraitValue(trait.traitValue) : false;
76
-
77
76
  }
@@ -27,21 +27,25 @@ export const matchingFunctions = {
27
27
  thisValue >= otherValue,
28
28
  [CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
29
29
  [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
30
- !!otherValue && otherValue.includes(thisValue),
30
+ !!otherValue && otherValue.includes(thisValue)
31
31
  };
32
32
 
33
33
  export const semverMatchingFunction = {
34
34
  ...matchingFunctions,
35
- [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => semver.eq(thisValue, otherValue),
36
- [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => semver.gt(otherValue, thisValue),
35
+ [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) =>
36
+ semver.eq(thisValue, otherValue),
37
+ [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) =>
38
+ semver.gt(otherValue, thisValue),
37
39
  [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
38
40
  semver.gte(otherValue, thisValue),
39
- [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => semver.gt(thisValue, otherValue),
41
+ [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) =>
42
+ semver.gt(thisValue, otherValue),
40
43
  [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
41
- semver.gte(thisValue, otherValue),
42
- }
44
+ semver.gte(thisValue, otherValue)
45
+ };
43
46
 
44
- export const getMatchingFunctions = (semver: boolean) => (semver ? semverMatchingFunction : matchingFunctions);
47
+ export const getMatchingFunctions = (semver: boolean) =>
48
+ semver ? semverMatchingFunction : matchingFunctions;
45
49
 
46
50
  export class SegmentConditionModel {
47
51
  EXCEPTION_OPERATOR_METHODS: { [key: string]: string } = {
@@ -55,7 +59,11 @@ export class SegmentConditionModel {
55
59
  value: string | null | undefined;
56
60
  property_: string | null | undefined;
57
61
 
58
- constructor(operator: string, value?: string | null | undefined, property?: string | null | undefined) {
62
+ constructor(
63
+ operator: string,
64
+ value?: string | null | undefined,
65
+ property?: string | null | undefined
66
+ ) {
59
67
  this.operator = operator;
60
68
  this.value = value;
61
69
  this.property_ = property;
@@ -64,24 +72,26 @@ export class SegmentConditionModel {
64
72
  matchesTraitValue(traitValue: any) {
65
73
  const evaluators: { [key: string]: CallableFunction } = {
66
74
  evaluateNotContains: (traitValue: any) => {
67
- return typeof traitValue == "string" &&
75
+ return (
76
+ typeof traitValue == 'string' &&
68
77
  !!this.value &&
69
- !traitValue.includes(this.value?.toString());
78
+ !traitValue.includes(this.value?.toString())
79
+ );
70
80
  },
71
81
  evaluateRegex: (traitValue: any) => {
72
82
  return !!this.value && !!traitValue?.toString().match(new RegExp(this.value));
73
83
  },
74
84
  evaluateModulo: (traitValue: any) => {
75
85
  if (isNaN(parseFloat(traitValue)) || !this.value) {
76
- return false
86
+ return false;
77
87
  }
78
- const parts = (this.value).split("|");
88
+ const parts = this.value.split('|');
79
89
  const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])];
80
- return traitValue % divisor === reminder
90
+ return traitValue % divisor === reminder;
81
91
  },
82
92
  evaluateIn: (traitValue: any) => {
83
- return this.value?.split(',').includes(traitValue.toString())
84
- },
93
+ return this.value?.split(',').includes(traitValue.toString());
94
+ }
85
95
  };
86
96
 
87
97
  // TODO: move this logic to the evaluator module
@@ -1,6 +1,6 @@
1
- import {BinaryLike, createHash} from "node:crypto";
1
+ import { BinaryLike, createHash } from 'node:crypto';
2
2
 
3
- const md5 = (data: BinaryLike) => createHash('md5').update(data).digest('hex')
3
+ const md5 = (data: BinaryLike) => createHash('md5').update(data).digest('hex');
4
4
 
5
5
  const makeRepeated = (arr: Array<any>, repeats: number) =>
6
6
  Array.from({ length: repeats }, () => arr).flat();
@@ -18,7 +18,7 @@ export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations =
18
18
  let toHash = makeRepeated(objectIds, iterations).join(',');
19
19
  const hashedValue = md5(toHash);
20
20
  const hashedInt = BigInt('0x' + hashedValue);
21
- const value = (Number((hashedInt % 9999n)) / 9998.0) * 100;
21
+ const value = (Number(hashedInt % 9999n) / 9998.0) * 100;
22
22
 
23
23
  // we ignore this for it's nearly impossible use case to catch
24
24
  /* istanbul ignore next */
@@ -1,6 +1,8 @@
1
- import { removeSemverSuffix } from "../segments/util.js";
1
+ import { removeSemverSuffix } from '../segments/util.js';
2
2
 
3
- export function getCastingFunction(traitType: 'boolean' | 'string' | 'number' | 'semver' | any): CallableFunction {
3
+ export function getCastingFunction(
4
+ traitType: 'boolean' | 'string' | 'number' | 'semver' | any
5
+ ): CallableFunction {
4
6
  switch (traitType) {
5
7
  case 'boolean':
6
8
  return (x: any) => !['False', 'false'].includes(x);